react-arborist 3.10.5 → 3.10.6
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/dist/main/interfaces/tree-api.d.ts +6 -0
- package/dist/main/interfaces/tree-api.js +27 -0
- package/dist/main/interfaces/tree-api.test.js +52 -0
- package/dist/module/interfaces/tree-api.d.ts +6 -0
- package/dist/module/interfaces/tree-api.js +27 -0
- package/dist/module/interfaces/tree-api.test.js +52 -0
- package/package.json +1 -1
- package/src/interfaces/tree-api.test.ts +60 -0
- package/src/interfaces/tree-api.ts +26 -0
|
@@ -229,6 +229,12 @@ export declare class TreeApi<T> {
|
|
|
229
229
|
openAll(): void;
|
|
230
230
|
closeAll(): void;
|
|
231
231
|
scrollTo(identity: Identity | T, align?: Align): Promise<void> | undefined;
|
|
232
|
+
/**
|
|
233
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
234
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
235
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
236
|
+
*/
|
|
237
|
+
private scrollToNodeHorizontally;
|
|
232
238
|
get isEditing(): boolean;
|
|
233
239
|
get isFiltered(): boolean;
|
|
234
240
|
get hasFocus(): boolean;
|
|
@@ -683,11 +683,38 @@ class TreeApi {
|
|
|
683
683
|
if (index === undefined)
|
|
684
684
|
return;
|
|
685
685
|
(_a = this.list.current) === null || _a === void 0 ? void 0 : _a.scrollToItem(index, align);
|
|
686
|
+
/* react-window only scrolls vertically. A deeply nested node is
|
|
687
|
+
indented by level * indent and can sit past the right edge when rows
|
|
688
|
+
overflow horizontally, so bring it into view ourselves (#220). */
|
|
689
|
+
this.scrollToNodeHorizontally(this.get(id));
|
|
686
690
|
})
|
|
687
691
|
.catch(() => {
|
|
688
692
|
// Id: ${id} never appeared in the list.
|
|
689
693
|
});
|
|
690
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
697
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
698
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
699
|
+
*/
|
|
700
|
+
scrollToNodeHorizontally(node) {
|
|
701
|
+
const el = this.listEl.current;
|
|
702
|
+
if (!node || !el)
|
|
703
|
+
return;
|
|
704
|
+
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
705
|
+
if (maxScroll <= 0)
|
|
706
|
+
return; // nothing to scroll
|
|
707
|
+
const left = node.level * this.indent;
|
|
708
|
+
const viewLeft = el.scrollLeft;
|
|
709
|
+
const viewRight = el.scrollLeft + el.clientWidth;
|
|
710
|
+
/* The visible range is half-open [viewLeft, viewRight): a pixel at viewRight
|
|
711
|
+
is already clipped. Only move when the node's indentation falls outside
|
|
712
|
+
it, aligning its content start to the left edge so the label is revealed,
|
|
713
|
+
clamped to the list's scrollable range. */
|
|
714
|
+
if (left < viewLeft || left >= viewRight) {
|
|
715
|
+
el.scrollLeft = Math.max(0, Math.min(left, maxScroll));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
691
718
|
/* State Checks */
|
|
692
719
|
get isEditing() {
|
|
693
720
|
return this.state.nodes.edit.id !== null;
|
|
@@ -167,3 +167,55 @@ describe("onSelect fires exactly once per selection method (#332)", () => {
|
|
|
167
167
|
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
|
+
describe("scrollTo brings a deeply nested node into view horizontally (#220)", () => {
|
|
171
|
+
// A folder tree where "deep" sits at level 2 (indented 2 * 24 = 48px).
|
|
172
|
+
const nestedData = [{ id: "root", children: [{ id: "mid", children: [{ id: "deep" }] }] }];
|
|
173
|
+
// react-window's scrollToItem only scrolls vertically; the horizontal scroll
|
|
174
|
+
// happens on the outer list element, which we stub here.
|
|
175
|
+
function setupWithListEl(el) {
|
|
176
|
+
const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
|
|
177
|
+
const list = { current: { scrollToItem: jest.fn() } };
|
|
178
|
+
const listEl = { current: el };
|
|
179
|
+
return new tree_api_1.TreeApi(store, { data: nestedData }, list, listEl);
|
|
180
|
+
}
|
|
181
|
+
test("scrolls right when the node is past the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
+
const el = { scrollWidth: 500, clientWidth: 40, scrollLeft: 0 };
|
|
183
|
+
const api = setupWithListEl(el);
|
|
184
|
+
yield api.scrollTo("deep");
|
|
185
|
+
// Aligns the node's indentation start (level 2 * indent 24) to the left edge.
|
|
186
|
+
expect(el.scrollLeft).toBe(48);
|
|
187
|
+
}));
|
|
188
|
+
test("scrolls left when the node is past the left edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
189
|
+
const el = { scrollWidth: 500, clientWidth: 100, scrollLeft: 200 };
|
|
190
|
+
const api = setupWithListEl(el);
|
|
191
|
+
yield api.scrollTo("deep");
|
|
192
|
+
expect(el.scrollLeft).toBe(48);
|
|
193
|
+
}));
|
|
194
|
+
test("scrolls when the node's start sits exactly on the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
// viewRight === left (48): the visible range is half-open, so the content
|
|
196
|
+
// start is already clipped and must be scrolled into view.
|
|
197
|
+
const el = { scrollWidth: 500, clientWidth: 48, scrollLeft: 0 };
|
|
198
|
+
const api = setupWithListEl(el);
|
|
199
|
+
yield api.scrollTo("deep");
|
|
200
|
+
expect(el.scrollLeft).toBe(48);
|
|
201
|
+
}));
|
|
202
|
+
test("clamps the target to the maximum scrollable distance", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
203
|
+
// left (48) exceeds maxScroll (60 - 40 = 20), so scrollLeft is clamped.
|
|
204
|
+
const el = { scrollWidth: 60, clientWidth: 40, scrollLeft: 0 };
|
|
205
|
+
const api = setupWithListEl(el);
|
|
206
|
+
yield api.scrollTo("deep");
|
|
207
|
+
expect(el.scrollLeft).toBe(20);
|
|
208
|
+
}));
|
|
209
|
+
test("leaves scroll untouched when the node is already in view", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
210
|
+
const el = { scrollWidth: 500, clientWidth: 200, scrollLeft: 0 };
|
|
211
|
+
const api = setupWithListEl(el);
|
|
212
|
+
yield api.scrollTo("deep");
|
|
213
|
+
expect(el.scrollLeft).toBe(0);
|
|
214
|
+
}));
|
|
215
|
+
test("no-ops when the list does not overflow horizontally", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
|
+
const el = { scrollWidth: 100, clientWidth: 100, scrollLeft: 0 };
|
|
217
|
+
const api = setupWithListEl(el);
|
|
218
|
+
yield api.scrollTo("deep");
|
|
219
|
+
expect(el.scrollLeft).toBe(0);
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
@@ -229,6 +229,12 @@ export declare class TreeApi<T> {
|
|
|
229
229
|
openAll(): void;
|
|
230
230
|
closeAll(): void;
|
|
231
231
|
scrollTo(identity: Identity | T, align?: Align): Promise<void> | undefined;
|
|
232
|
+
/**
|
|
233
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
234
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
235
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
236
|
+
*/
|
|
237
|
+
private scrollToNodeHorizontally;
|
|
232
238
|
get isEditing(): boolean;
|
|
233
239
|
get isFiltered(): boolean;
|
|
234
240
|
get hasFocus(): boolean;
|
|
@@ -657,11 +657,38 @@ export class TreeApi {
|
|
|
657
657
|
if (index === undefined)
|
|
658
658
|
return;
|
|
659
659
|
(_a = this.list.current) === null || _a === void 0 ? void 0 : _a.scrollToItem(index, align);
|
|
660
|
+
/* react-window only scrolls vertically. A deeply nested node is
|
|
661
|
+
indented by level * indent and can sit past the right edge when rows
|
|
662
|
+
overflow horizontally, so bring it into view ourselves (#220). */
|
|
663
|
+
this.scrollToNodeHorizontally(this.get(id));
|
|
660
664
|
})
|
|
661
665
|
.catch(() => {
|
|
662
666
|
// Id: ${id} never appeared in the list.
|
|
663
667
|
});
|
|
664
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
671
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
672
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
673
|
+
*/
|
|
674
|
+
scrollToNodeHorizontally(node) {
|
|
675
|
+
const el = this.listEl.current;
|
|
676
|
+
if (!node || !el)
|
|
677
|
+
return;
|
|
678
|
+
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
679
|
+
if (maxScroll <= 0)
|
|
680
|
+
return; // nothing to scroll
|
|
681
|
+
const left = node.level * this.indent;
|
|
682
|
+
const viewLeft = el.scrollLeft;
|
|
683
|
+
const viewRight = el.scrollLeft + el.clientWidth;
|
|
684
|
+
/* The visible range is half-open [viewLeft, viewRight): a pixel at viewRight
|
|
685
|
+
is already clipped. Only move when the node's indentation falls outside
|
|
686
|
+
it, aligning its content start to the left edge so the label is revealed,
|
|
687
|
+
clamped to the list's scrollable range. */
|
|
688
|
+
if (left < viewLeft || left >= viewRight) {
|
|
689
|
+
el.scrollLeft = Math.max(0, Math.min(left, maxScroll));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
665
692
|
/* State Checks */
|
|
666
693
|
get isEditing() {
|
|
667
694
|
return this.state.nodes.edit.id !== null;
|
|
@@ -165,3 +165,55 @@ describe("onSelect fires exactly once per selection method (#332)", () => {
|
|
|
165
165
|
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
|
+
describe("scrollTo brings a deeply nested node into view horizontally (#220)", () => {
|
|
169
|
+
// A folder tree where "deep" sits at level 2 (indented 2 * 24 = 48px).
|
|
170
|
+
const nestedData = [{ id: "root", children: [{ id: "mid", children: [{ id: "deep" }] }] }];
|
|
171
|
+
// react-window's scrollToItem only scrolls vertically; the horizontal scroll
|
|
172
|
+
// happens on the outer list element, which we stub here.
|
|
173
|
+
function setupWithListEl(el) {
|
|
174
|
+
const store = createStore(rootReducer);
|
|
175
|
+
const list = { current: { scrollToItem: jest.fn() } };
|
|
176
|
+
const listEl = { current: el };
|
|
177
|
+
return new TreeApi(store, { data: nestedData }, list, listEl);
|
|
178
|
+
}
|
|
179
|
+
test("scrolls right when the node is past the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
const el = { scrollWidth: 500, clientWidth: 40, scrollLeft: 0 };
|
|
181
|
+
const api = setupWithListEl(el);
|
|
182
|
+
yield api.scrollTo("deep");
|
|
183
|
+
// Aligns the node's indentation start (level 2 * indent 24) to the left edge.
|
|
184
|
+
expect(el.scrollLeft).toBe(48);
|
|
185
|
+
}));
|
|
186
|
+
test("scrolls left when the node is past the left edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
187
|
+
const el = { scrollWidth: 500, clientWidth: 100, scrollLeft: 200 };
|
|
188
|
+
const api = setupWithListEl(el);
|
|
189
|
+
yield api.scrollTo("deep");
|
|
190
|
+
expect(el.scrollLeft).toBe(48);
|
|
191
|
+
}));
|
|
192
|
+
test("scrolls when the node's start sits exactly on the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
// viewRight === left (48): the visible range is half-open, so the content
|
|
194
|
+
// start is already clipped and must be scrolled into view.
|
|
195
|
+
const el = { scrollWidth: 500, clientWidth: 48, scrollLeft: 0 };
|
|
196
|
+
const api = setupWithListEl(el);
|
|
197
|
+
yield api.scrollTo("deep");
|
|
198
|
+
expect(el.scrollLeft).toBe(48);
|
|
199
|
+
}));
|
|
200
|
+
test("clamps the target to the maximum scrollable distance", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
201
|
+
// left (48) exceeds maxScroll (60 - 40 = 20), so scrollLeft is clamped.
|
|
202
|
+
const el = { scrollWidth: 60, clientWidth: 40, scrollLeft: 0 };
|
|
203
|
+
const api = setupWithListEl(el);
|
|
204
|
+
yield api.scrollTo("deep");
|
|
205
|
+
expect(el.scrollLeft).toBe(20);
|
|
206
|
+
}));
|
|
207
|
+
test("leaves scroll untouched when the node is already in view", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
208
|
+
const el = { scrollWidth: 500, clientWidth: 200, scrollLeft: 0 };
|
|
209
|
+
const api = setupWithListEl(el);
|
|
210
|
+
yield api.scrollTo("deep");
|
|
211
|
+
expect(el.scrollLeft).toBe(0);
|
|
212
|
+
}));
|
|
213
|
+
test("no-ops when the list does not overflow horizontally", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
|
+
const el = { scrollWidth: 100, clientWidth: 100, scrollLeft: 0 };
|
|
215
|
+
const api = setupWithListEl(el);
|
|
216
|
+
yield api.scrollTo("deep");
|
|
217
|
+
expect(el.scrollLeft).toBe(0);
|
|
218
|
+
}));
|
|
219
|
+
});
|
package/package.json
CHANGED
|
@@ -184,3 +184,63 @@ describe("onSelect fires exactly once per selection method (#332)", () => {
|
|
|
184
184
|
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
185
185
|
});
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
describe("scrollTo brings a deeply nested node into view horizontally (#220)", () => {
|
|
189
|
+
// A folder tree where "deep" sits at level 2 (indented 2 * 24 = 48px).
|
|
190
|
+
const nestedData = [{ id: "root", children: [{ id: "mid", children: [{ id: "deep" }] }] }];
|
|
191
|
+
|
|
192
|
+
// react-window's scrollToItem only scrolls vertically; the horizontal scroll
|
|
193
|
+
// happens on the outer list element, which we stub here.
|
|
194
|
+
function setupWithListEl(el: Partial<HTMLDivElement>) {
|
|
195
|
+
const store = createStore(rootReducer);
|
|
196
|
+
const list = { current: { scrollToItem: jest.fn() } as any };
|
|
197
|
+
const listEl = { current: el as HTMLDivElement };
|
|
198
|
+
return new TreeApi(store, { data: nestedData }, list, listEl);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
test("scrolls right when the node is past the right edge", async () => {
|
|
202
|
+
const el = { scrollWidth: 500, clientWidth: 40, scrollLeft: 0 };
|
|
203
|
+
const api = setupWithListEl(el);
|
|
204
|
+
await api.scrollTo("deep");
|
|
205
|
+
// Aligns the node's indentation start (level 2 * indent 24) to the left edge.
|
|
206
|
+
expect(el.scrollLeft).toBe(48);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("scrolls left when the node is past the left edge", async () => {
|
|
210
|
+
const el = { scrollWidth: 500, clientWidth: 100, scrollLeft: 200 };
|
|
211
|
+
const api = setupWithListEl(el);
|
|
212
|
+
await api.scrollTo("deep");
|
|
213
|
+
expect(el.scrollLeft).toBe(48);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("scrolls when the node's start sits exactly on the right edge", async () => {
|
|
217
|
+
// viewRight === left (48): the visible range is half-open, so the content
|
|
218
|
+
// start is already clipped and must be scrolled into view.
|
|
219
|
+
const el = { scrollWidth: 500, clientWidth: 48, scrollLeft: 0 };
|
|
220
|
+
const api = setupWithListEl(el);
|
|
221
|
+
await api.scrollTo("deep");
|
|
222
|
+
expect(el.scrollLeft).toBe(48);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("clamps the target to the maximum scrollable distance", async () => {
|
|
226
|
+
// left (48) exceeds maxScroll (60 - 40 = 20), so scrollLeft is clamped.
|
|
227
|
+
const el = { scrollWidth: 60, clientWidth: 40, scrollLeft: 0 };
|
|
228
|
+
const api = setupWithListEl(el);
|
|
229
|
+
await api.scrollTo("deep");
|
|
230
|
+
expect(el.scrollLeft).toBe(20);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("leaves scroll untouched when the node is already in view", async () => {
|
|
234
|
+
const el = { scrollWidth: 500, clientWidth: 200, scrollLeft: 0 };
|
|
235
|
+
const api = setupWithListEl(el);
|
|
236
|
+
await api.scrollTo("deep");
|
|
237
|
+
expect(el.scrollLeft).toBe(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("no-ops when the list does not overflow horizontally", async () => {
|
|
241
|
+
const el = { scrollWidth: 100, clientWidth: 100, scrollLeft: 0 };
|
|
242
|
+
const api = setupWithListEl(el);
|
|
243
|
+
await api.scrollTo("deep");
|
|
244
|
+
expect(el.scrollLeft).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -675,12 +675,38 @@ export class TreeApi<T> {
|
|
|
675
675
|
const index = this.idToIndex[id];
|
|
676
676
|
if (index === undefined) return;
|
|
677
677
|
this.list.current?.scrollToItem(index, align);
|
|
678
|
+
/* react-window only scrolls vertically. A deeply nested node is
|
|
679
|
+
indented by level * indent and can sit past the right edge when rows
|
|
680
|
+
overflow horizontally, so bring it into view ourselves (#220). */
|
|
681
|
+
this.scrollToNodeHorizontally(this.get(id));
|
|
678
682
|
})
|
|
679
683
|
.catch(() => {
|
|
680
684
|
// Id: ${id} never appeared in the list.
|
|
681
685
|
});
|
|
682
686
|
}
|
|
683
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
690
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
691
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
692
|
+
*/
|
|
693
|
+
private scrollToNodeHorizontally(node: NodeApi<T> | null) {
|
|
694
|
+
const el = this.listEl.current;
|
|
695
|
+
if (!node || !el) return;
|
|
696
|
+
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
697
|
+
if (maxScroll <= 0) return; // nothing to scroll
|
|
698
|
+
const left = node.level * this.indent;
|
|
699
|
+
const viewLeft = el.scrollLeft;
|
|
700
|
+
const viewRight = el.scrollLeft + el.clientWidth;
|
|
701
|
+
/* The visible range is half-open [viewLeft, viewRight): a pixel at viewRight
|
|
702
|
+
is already clipped. Only move when the node's indentation falls outside
|
|
703
|
+
it, aligning its content start to the left edge so the label is revealed,
|
|
704
|
+
clamped to the list's scrollable range. */
|
|
705
|
+
if (left < viewLeft || left >= viewRight) {
|
|
706
|
+
el.scrollLeft = Math.max(0, Math.min(left, maxScroll));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
684
710
|
/* State Checks */
|
|
685
711
|
|
|
686
712
|
get isEditing() {
|