react-arborist 3.10.4 → 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.
@@ -149,6 +149,19 @@ export declare class TreeApi<T> {
149
149
  get matchFn(): (node: NodeApi<T>) => boolean;
150
150
  accessChildren(data: T): readonly T[] | null;
151
151
  accessId(data: T): string;
152
+ /**
153
+ * Resolve an identifier to a node id. Public methods accept an id string, a
154
+ * NodeApi, or the raw row data; this is the one place that turns any of those
155
+ * into the string id used internally. Raw data is run through the configured
156
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
157
+ * just where nodes were built. A NodeApi already carries its accessor-derived
158
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
159
+ * underlying data, which a NodeApi does not expose under that key). Unlike
160
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
161
+ * preserving the previous behavior of the `id`-only lookup.
162
+ */
163
+ identify(identity: string | IdObj | T): string;
164
+ identifyNull(identity: Identity | T): string | null;
152
165
  get firstNode(): NodeApi<T>;
153
166
  get lastNode(): NodeApi<T>;
154
167
  get focusedNode(): NodeApi<T> | null;
@@ -158,7 +171,7 @@ export declare class TreeApi<T> {
158
171
  get(id: string | null): NodeApi<T> | null;
159
172
  at(index: number): NodeApi<T> | null;
160
173
  nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
161
- indexOf(id: Identity): number | null;
174
+ indexOf(id: Identity | T): number | null;
162
175
  get editingId(): string | null;
163
176
  createInternal(): Promise<void>;
164
177
  createLeaf(): Promise<void>;
@@ -167,36 +180,36 @@ export declare class TreeApi<T> {
167
180
  parentId?: null | string;
168
181
  index?: null | number;
169
182
  }): Promise<void>;
170
- delete(node: Identity | string[] | IdObj[]): Promise<void>;
171
- edit(node: string | IdObj): Promise<EditResult>;
172
- submit(identity: Identity, value: string): Promise<void>;
183
+ delete(node: Identity | T | (string | IdObj | T)[]): Promise<void>;
184
+ edit(node: string | IdObj | T): Promise<EditResult>;
185
+ submit(identity: Identity | T, value: string): Promise<void>;
173
186
  reset(): void;
174
- activate(id: Identity): void;
187
+ activate(id: Identity | T): void;
175
188
  private resolveEdit;
176
189
  get selectedIds(): Set<string>;
177
190
  get selectedNodes(): NodeApi<T>[];
178
- focus(node: Identity, opts?: {
191
+ focus(node: Identity | T, opts?: {
179
192
  scroll?: boolean;
180
193
  }): void;
181
194
  pageUp(): void;
182
195
  pageDown(): void;
183
- select(node: Identity, opts?: {
196
+ select(node: Identity | T, opts?: {
184
197
  align?: Align;
185
198
  focus?: boolean;
186
199
  }): void;
187
- deselect(node: Identity): void;
188
- selectMulti(identity: Identity, opts?: {
200
+ deselect(node: Identity | T): void;
201
+ selectMulti(identity: Identity | T, opts?: {
189
202
  align?: Align;
190
203
  focus?: boolean;
191
204
  }): void;
192
- selectContiguous(identity: Identity): void;
205
+ selectContiguous(identity: Identity | T): void;
193
206
  deselectAll(): void;
194
207
  selectAll(): void;
195
208
  private filterSelectableNodes;
196
209
  setSelection(args: {
197
- ids: (IdObj | string)[] | null;
198
- anchor: Identity;
199
- mostRecent: Identity;
210
+ ids: (IdObj | string | T)[] | null;
211
+ anchor: Identity | T;
212
+ mostRecent: Identity | T;
200
213
  }): void;
201
214
  get cursorParentId(): string | null;
202
215
  get cursorOverFolder(): boolean;
@@ -208,14 +221,20 @@ export declare class TreeApi<T> {
208
221
  drop(): void;
209
222
  hideCursor(): void;
210
223
  showCursor(cursor: Cursor): void;
211
- open(identity: Identity, redraw?: boolean): void;
212
- close(identity: Identity, redraw?: boolean): void;
213
- toggle(identity: Identity): void;
214
- openParents(identity: Identity): void;
224
+ open(identity: Identity | T, redraw?: boolean): void;
225
+ close(identity: Identity | T, redraw?: boolean): void;
226
+ toggle(identity: Identity | T): void;
227
+ openParents(identity: Identity | T): void;
215
228
  openSiblings(node: NodeApi<T>): void;
216
229
  openAll(): void;
217
230
  closeAll(): void;
218
- scrollTo(identity: Identity, align?: Align): Promise<void> | undefined;
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;
219
238
  get isEditing(): boolean;
220
239
  get isFiltered(): boolean;
221
240
  get hasFocus(): boolean;
@@ -228,10 +247,10 @@ export declare class TreeApi<T> {
228
247
  isDraggable(data: T): boolean;
229
248
  isSelectable(data: T): boolean;
230
249
  private isActionPossible;
231
- isDragging(node: Identity): boolean;
250
+ isDragging(node: Identity | T): boolean;
232
251
  isFocused(id: string): boolean;
233
252
  isMatch(node: NodeApi<T>): boolean;
234
- willReceiveDrop(node: Identity): boolean;
253
+ willReceiveDrop(node: Identity | T): boolean;
235
254
  onFocus(): void;
236
255
  onBlur(): void;
237
256
  onItemsRendered(args: ListOnItemsRenderedProps): void;
@@ -37,6 +37,7 @@ const utils = __importStar(require("../utils"));
37
37
  const default_cursor_1 = require("../components/default-cursor");
38
38
  const default_row_1 = require("../components/default-row");
39
39
  const default_node_1 = require("../components/default-node");
40
+ const node_api_1 = require("./node-api");
40
41
  const edit_slice_1 = require("../state/edit-slice");
41
42
  const focus_slice_1 = require("../state/focus-slice");
42
43
  const create_root_1 = require("../data/create-root");
@@ -47,7 +48,7 @@ const default_drag_preview_1 = require("../components/default-drag-preview");
47
48
  const default_container_1 = require("../components/default-container");
48
49
  const create_list_1 = require("../data/create-list");
49
50
  const create_index_1 = require("../data/create-index");
50
- const { safeRun, identify, identifyNull } = utils;
51
+ const { safeRun } = utils;
51
52
  class TreeApi {
52
53
  constructor(store, props, list, listEl) {
53
54
  this.store = store;
@@ -189,6 +190,30 @@ class TreeApi {
189
190
  throw new Error("Data must contain an 'id' property or props.idAccessor must return a string");
190
191
  return id;
191
192
  }
193
+ /**
194
+ * Resolve an identifier to a node id. Public methods accept an id string, a
195
+ * NodeApi, or the raw row data; this is the one place that turns any of those
196
+ * into the string id used internally. Raw data is run through the configured
197
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
198
+ * just where nodes were built. A NodeApi already carries its accessor-derived
199
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
200
+ * underlying data, which a NodeApi does not expose under that key). Unlike
201
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
202
+ * preserving the previous behavior of the `id`-only lookup.
203
+ */
204
+ identify(identity) {
205
+ if (typeof identity === "string")
206
+ return identity;
207
+ if (identity instanceof node_api_1.NodeApi)
208
+ return identity.id;
209
+ const get = this.props.idAccessor || "id";
210
+ return utils.access(identity, get);
211
+ }
212
+ identifyNull(identity) {
213
+ if (identity === null || identity === undefined)
214
+ return null;
215
+ return this.identify(identity);
216
+ }
192
217
  /* Node Access */
193
218
  get firstNode() {
194
219
  var _a;
@@ -244,7 +269,7 @@ class TreeApi {
244
269
  return this.visibleNodes.slice(start, end + 1);
245
270
  }
246
271
  indexOf(id) {
247
- const key = utils.identifyNull(id);
272
+ const key = this.identifyNull(id);
248
273
  if (!key)
249
274
  return null;
250
275
  return this.idToIndex[key];
@@ -287,7 +312,7 @@ class TreeApi {
287
312
  if (!node)
288
313
  return;
289
314
  const idents = Array.isArray(node) ? node : [node];
290
- const ids = idents.map(identify);
315
+ const ids = idents.map((i) => this.identify(i));
291
316
  const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
292
317
  /* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
293
318
  const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
@@ -297,7 +322,7 @@ class TreeApi {
297
322
  }
298
323
  edit(node) {
299
324
  var _a, _b;
300
- const id = identify(node);
325
+ const id = this.identify(node);
301
326
  this.resolveEdit({ cancelled: true });
302
327
  this.scrollTo(id);
303
328
  this.dispatch((0, edit_slice_1.edit)(id));
@@ -311,7 +336,7 @@ class TreeApi {
311
336
  var _a, _b;
312
337
  if (!identity)
313
338
  return;
314
- const id = identify(identity);
339
+ const id = this.identify(identity);
315
340
  yield safeRun(this.props.onRename, {
316
341
  id,
317
342
  name: value,
@@ -330,7 +355,7 @@ class TreeApi {
330
355
  setTimeout(() => this.onFocus()); // Return focus to element;
331
356
  }
332
357
  activate(id) {
333
- const node = this.get(identifyNull(id));
358
+ const node = this.get(this.identifyNull(id));
334
359
  if (!node)
335
360
  return;
336
361
  safeRun(this.props.onActivate, node);
@@ -364,7 +389,7 @@ class TreeApi {
364
389
  this.select(node);
365
390
  }
366
391
  else {
367
- this.dispatch((0, focus_slice_1.focus)(identify(node)));
392
+ this.dispatch((0, focus_slice_1.focus)(this.identify(node)));
368
393
  if (opts.scroll !== false)
369
394
  this.scrollTo(node);
370
395
  if (this.focusedNode)
@@ -404,7 +429,7 @@ class TreeApi {
404
429
  if (!node)
405
430
  return;
406
431
  const changeFocus = opts.focus !== false;
407
- const id = identify(node);
432
+ const id = this.identify(node);
408
433
  if (changeFocus)
409
434
  this.dispatch((0, focus_slice_1.focus)(id));
410
435
  if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
@@ -422,12 +447,12 @@ class TreeApi {
422
447
  deselect(node) {
423
448
  if (!node)
424
449
  return;
425
- const id = identify(node);
450
+ const id = this.identify(node);
426
451
  this.dispatch(selection_slice_1.actions.remove(id));
427
452
  safeRun(this.props.onSelect, this.selectedNodes);
428
453
  }
429
454
  selectMulti(identity, opts = {}) {
430
- const node = this.get(identifyNull(identity));
455
+ const node = this.get(this.identifyNull(identity));
431
456
  if (!node)
432
457
  return;
433
458
  const changeFocus = opts.focus !== false;
@@ -448,11 +473,11 @@ class TreeApi {
448
473
  var _a;
449
474
  if (!identity)
450
475
  return;
451
- const id = identify(identity);
476
+ const id = this.identify(identity);
452
477
  this.dispatch((0, focus_slice_1.focus)(id));
453
478
  if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
454
479
  const { anchor, mostRecent } = this.state.nodes.selection;
455
- const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
480
+ const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, this.identifyNull(id)));
456
481
  this.dispatch(selection_slice_1.actions.remove(this.nodesBetween(anchor, mostRecent)));
457
482
  this.dispatch(selection_slice_1.actions.add(selectableNodes));
458
483
  this.dispatch(selection_slice_1.actions.mostRecent(id));
@@ -481,14 +506,14 @@ class TreeApi {
481
506
  }
482
507
  filterSelectableNodes(nodes) {
483
508
  return nodes
484
- .map((n) => this.get(identify(n)))
509
+ .map((n) => this.get(this.identify(n)))
485
510
  .filter((n) => !!n && n.isSelectable);
486
511
  }
487
512
  setSelection(args) {
488
513
  var _a;
489
- const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
490
- const anchor = identifyNull(args.anchor);
491
- const mostRecent = identifyNull(args.mostRecent);
514
+ const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map((i) => this.identify(i)));
515
+ const anchor = this.identifyNull(args.anchor);
516
+ const mostRecent = this.identifyNull(args.mostRecent);
492
517
  this.dispatch(selection_slice_1.actions.set({ ids, anchor, mostRecent }));
493
518
  safeRun(this.props.onSelect, this.selectedNodes);
494
519
  }
@@ -571,7 +596,7 @@ class TreeApi {
571
596
  /* Visibility */
572
597
  open(identity, redraw = true) {
573
598
  var _a, _b;
574
- const id = identifyNull(identity);
599
+ const id = this.identifyNull(identity);
575
600
  if (!id)
576
601
  return;
577
602
  if (this.isOpen(id))
@@ -583,7 +608,7 @@ class TreeApi {
583
608
  }
584
609
  close(identity, redraw = true) {
585
610
  var _a, _b;
586
- const id = identifyNull(identity);
611
+ const id = this.identifyNull(identity);
587
612
  if (!id)
588
613
  return;
589
614
  if (!this.isOpen(id))
@@ -594,13 +619,13 @@ class TreeApi {
594
619
  safeRun(this.props.onToggle, id);
595
620
  }
596
621
  toggle(identity) {
597
- const id = identifyNull(identity);
622
+ const id = this.identifyNull(identity);
598
623
  if (!id)
599
624
  return;
600
625
  return this.isOpen(id) ? this.close(id) : this.open(id);
601
626
  }
602
627
  openParents(identity) {
603
- const id = identifyNull(identity);
628
+ const id = this.identifyNull(identity);
604
629
  if (!id)
605
630
  return;
606
631
  const node = utils.dfs(this.root, id);
@@ -648,7 +673,7 @@ class TreeApi {
648
673
  scrollTo(identity, align = "smart") {
649
674
  if (!identity)
650
675
  return;
651
- const id = identify(identity);
676
+ const id = this.identify(identity);
652
677
  this.openParents(id);
653
678
  return utils
654
679
  .waitFor(() => id in this.idToIndex)
@@ -658,11 +683,38 @@ class TreeApi {
658
683
  if (index === undefined)
659
684
  return;
660
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));
661
690
  })
662
691
  .catch(() => {
663
692
  // Id: ${id} never appeared in the list.
664
693
  });
665
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
+ }
666
718
  /* State Checks */
667
719
  get isEditing() {
668
720
  return this.state.nodes.edit.id !== null;
@@ -715,7 +767,7 @@ class TreeApi {
715
767
  return !utils.access(data, disabler);
716
768
  }
717
769
  isDragging(node) {
718
- const id = identifyNull(node);
770
+ const id = this.identifyNull(node);
719
771
  if (!id)
720
772
  return false;
721
773
  return this.state.nodes.drag.id === id;
@@ -727,7 +779,7 @@ class TreeApi {
727
779
  return this.matchFn(node);
728
780
  }
729
781
  willReceiveDrop(node) {
730
- const id = identifyNull(node);
782
+ const id = this.identifyNull(node);
731
783
  if (!id)
732
784
  return false;
733
785
  const { destinationParentId, destinationIndex } = this.state.nodes.drag;
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  const redux_1 = require("redux");
4
13
  const root_reducer_1 = require("../state/root-reducer");
@@ -38,6 +47,45 @@ describe("tree.drop() fires onMove (#313)", () => {
38
47
  expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
39
48
  });
40
49
  });
50
+ describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
51
+ const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
52
+ test("select(data) resolves the id through idAccessor", () => {
53
+ const onSelect = jest.fn();
54
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
55
+ api.select(uuidData[1]);
56
+ expect(api.selectedIds.has("b")).toBe(true);
57
+ expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
58
+ });
59
+ test("focus(data) resolves the id through idAccessor", () => {
60
+ var _a;
61
+ const api = setupApi({ data: uuidData, idAccessor: "uuid" });
62
+ api.focus(uuidData[2]);
63
+ expect((_a = api.focusedNode) === null || _a === void 0 ? void 0 : _a.id).toBe("c");
64
+ });
65
+ test("delete(data) passes the accessor-derived id to onDelete", () => {
66
+ const onDelete = jest.fn();
67
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
68
+ api.delete(uuidData[0]);
69
+ expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ ids: ["a"] }));
70
+ });
71
+ test("create() focuses the new node by its accessor-derived id", () => __awaiter(void 0, void 0, void 0, function* () {
72
+ // create() passes the raw row data returned by onCreate straight to
73
+ // focus/edit/select; before the fix these read `.id` and lost the node.
74
+ const onCreate = () => ({ uuid: "new" });
75
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
76
+ yield api.create();
77
+ expect(api.state.nodes.focus.id).toBe("new");
78
+ }));
79
+ test("a function idAccessor is honored too", () => {
80
+ const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
81
+ const api = setupApi({
82
+ data: fnData,
83
+ idAccessor: (d) => d.meta.key,
84
+ });
85
+ api.select(fnData[1]);
86
+ expect(api.selectedIds.has("y")).toBe(true);
87
+ });
88
+ });
41
89
  test("rowHeight defaults to 24", () => {
42
90
  const api = setupApi({});
43
91
  expect(api.rowHeight).toBe(24);
@@ -119,3 +167,55 @@ describe("onSelect fires exactly once per selection method (#332)", () => {
119
167
  expect(onSelect).toHaveBeenCalledTimes(1);
120
168
  });
121
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
+ });
@@ -5,7 +5,7 @@ export type CreateHandler<T> = (args: {
5
5
  parentNode: NodeApi<T> | null;
6
6
  index: number;
7
7
  type: "internal" | "leaf";
8
- }) => (IdObj | null) | Promise<IdObj | null>;
8
+ }) => (T | IdObj | null) | Promise<T | IdObj | null>;
9
9
  export type MoveHandler<T> = (args: {
10
10
  dragIds: string[];
11
11
  dragNodes: NodeApi<T>[];
@@ -149,6 +149,19 @@ export declare class TreeApi<T> {
149
149
  get matchFn(): (node: NodeApi<T>) => boolean;
150
150
  accessChildren(data: T): readonly T[] | null;
151
151
  accessId(data: T): string;
152
+ /**
153
+ * Resolve an identifier to a node id. Public methods accept an id string, a
154
+ * NodeApi, or the raw row data; this is the one place that turns any of those
155
+ * into the string id used internally. Raw data is run through the configured
156
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
157
+ * just where nodes were built. A NodeApi already carries its accessor-derived
158
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
159
+ * underlying data, which a NodeApi does not expose under that key). Unlike
160
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
161
+ * preserving the previous behavior of the `id`-only lookup.
162
+ */
163
+ identify(identity: string | IdObj | T): string;
164
+ identifyNull(identity: Identity | T): string | null;
152
165
  get firstNode(): NodeApi<T>;
153
166
  get lastNode(): NodeApi<T>;
154
167
  get focusedNode(): NodeApi<T> | null;
@@ -158,7 +171,7 @@ export declare class TreeApi<T> {
158
171
  get(id: string | null): NodeApi<T> | null;
159
172
  at(index: number): NodeApi<T> | null;
160
173
  nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
161
- indexOf(id: Identity): number | null;
174
+ indexOf(id: Identity | T): number | null;
162
175
  get editingId(): string | null;
163
176
  createInternal(): Promise<void>;
164
177
  createLeaf(): Promise<void>;
@@ -167,36 +180,36 @@ export declare class TreeApi<T> {
167
180
  parentId?: null | string;
168
181
  index?: null | number;
169
182
  }): Promise<void>;
170
- delete(node: Identity | string[] | IdObj[]): Promise<void>;
171
- edit(node: string | IdObj): Promise<EditResult>;
172
- submit(identity: Identity, value: string): Promise<void>;
183
+ delete(node: Identity | T | (string | IdObj | T)[]): Promise<void>;
184
+ edit(node: string | IdObj | T): Promise<EditResult>;
185
+ submit(identity: Identity | T, value: string): Promise<void>;
173
186
  reset(): void;
174
- activate(id: Identity): void;
187
+ activate(id: Identity | T): void;
175
188
  private resolveEdit;
176
189
  get selectedIds(): Set<string>;
177
190
  get selectedNodes(): NodeApi<T>[];
178
- focus(node: Identity, opts?: {
191
+ focus(node: Identity | T, opts?: {
179
192
  scroll?: boolean;
180
193
  }): void;
181
194
  pageUp(): void;
182
195
  pageDown(): void;
183
- select(node: Identity, opts?: {
196
+ select(node: Identity | T, opts?: {
184
197
  align?: Align;
185
198
  focus?: boolean;
186
199
  }): void;
187
- deselect(node: Identity): void;
188
- selectMulti(identity: Identity, opts?: {
200
+ deselect(node: Identity | T): void;
201
+ selectMulti(identity: Identity | T, opts?: {
189
202
  align?: Align;
190
203
  focus?: boolean;
191
204
  }): void;
192
- selectContiguous(identity: Identity): void;
205
+ selectContiguous(identity: Identity | T): void;
193
206
  deselectAll(): void;
194
207
  selectAll(): void;
195
208
  private filterSelectableNodes;
196
209
  setSelection(args: {
197
- ids: (IdObj | string)[] | null;
198
- anchor: Identity;
199
- mostRecent: Identity;
210
+ ids: (IdObj | string | T)[] | null;
211
+ anchor: Identity | T;
212
+ mostRecent: Identity | T;
200
213
  }): void;
201
214
  get cursorParentId(): string | null;
202
215
  get cursorOverFolder(): boolean;
@@ -208,14 +221,20 @@ export declare class TreeApi<T> {
208
221
  drop(): void;
209
222
  hideCursor(): void;
210
223
  showCursor(cursor: Cursor): void;
211
- open(identity: Identity, redraw?: boolean): void;
212
- close(identity: Identity, redraw?: boolean): void;
213
- toggle(identity: Identity): void;
214
- openParents(identity: Identity): void;
224
+ open(identity: Identity | T, redraw?: boolean): void;
225
+ close(identity: Identity | T, redraw?: boolean): void;
226
+ toggle(identity: Identity | T): void;
227
+ openParents(identity: Identity | T): void;
215
228
  openSiblings(node: NodeApi<T>): void;
216
229
  openAll(): void;
217
230
  closeAll(): void;
218
- scrollTo(identity: Identity, align?: Align): Promise<void> | undefined;
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;
219
238
  get isEditing(): boolean;
220
239
  get isFiltered(): boolean;
221
240
  get hasFocus(): boolean;
@@ -228,10 +247,10 @@ export declare class TreeApi<T> {
228
247
  isDraggable(data: T): boolean;
229
248
  isSelectable(data: T): boolean;
230
249
  private isActionPossible;
231
- isDragging(node: Identity): boolean;
250
+ isDragging(node: Identity | T): boolean;
232
251
  isFocused(id: string): boolean;
233
252
  isMatch(node: NodeApi<T>): boolean;
234
- willReceiveDrop(node: Identity): boolean;
253
+ willReceiveDrop(node: Identity | T): boolean;
235
254
  onFocus(): void;
236
255
  onBlur(): void;
237
256
  onItemsRendered(args: ListOnItemsRenderedProps): void;