@jupyterlab/filebrowser 4.6.0-alpha.4 → 4.6.0-alpha.5

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/lib/crumbs.js CHANGED
@@ -5,6 +5,7 @@ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
5
5
  import { renameFile } from '@jupyterlab/docmanager';
6
6
  import { nullTranslator } from '@jupyterlab/translation';
7
7
  import { ellipsesIcon, homeIcon as preferredIcon, folderIcon as rootIcon } from '@jupyterlab/ui-components';
8
+ import { PathNavigator } from './pathnavigator';
8
9
  import { JSONExt } from '@lumino/coreutils';
9
10
  import { Throttler } from '@lumino/polling';
10
11
  import { Widget } from '@lumino/widgets';
@@ -40,6 +41,18 @@ const CONTENTS_MIME = 'application/x-jupyter-icontents';
40
41
  * The class name added to drop targets.
41
42
  */
42
43
  const DROP_TARGET_CLASS = 'jp-mod-dropTarget';
44
+ /**
45
+ * The class name for the container span that holds all breadcrumb items.
46
+ */
47
+ const BREADCRUMB_CONTAINER_CLASS = 'jp-BreadCrumbs-container';
48
+ /**
49
+ * The class name for the inner wrapper that hugs breadcrumb content.
50
+ */
51
+ const BREADCRUMB_CONTENT_CLASS = 'jp-BreadCrumbs-content';
52
+ /**
53
+ * The class name added to the breadcrumb node when in edit mode.
54
+ */
55
+ const BREADCRUMB_EDIT_MODE_CLASS = 'jp-mod-editMode';
43
56
  /**
44
57
  * A class which hosts folder breadcrumbs.
45
58
  */
@@ -55,21 +68,31 @@ export class BreadCrumbs extends Widget {
55
68
  this._previousState = null;
56
69
  this._cachedWidths = null;
57
70
  this._lastRenderedWidth = 0;
71
+ this._isEditMode = false;
72
+ this._lastPath = '';
73
+ /**
74
+ * After `cd()` rebuilds the trail, restore focus to the current-directory segment.
75
+ */
76
+ this._restoreBreadcrumbFocusAfterUpdate = false;
58
77
  this.translator = options.translator || nullTranslator;
59
78
  this._trans = this.translator.load('jupyterlab');
60
79
  this._model = options.model;
61
80
  this._fullPath = options.fullPath || false;
62
81
  this._minimumLeftItems = (_a = options.minimumLeftItems) !== null && _a !== void 0 ? _a : 0;
63
82
  this._minimumRightItems = (_b = options.minimumRightItems) !== null && _b !== void 0 ? _b : 2;
83
+ this._onPathEdited = options.onPathEdited;
84
+ this._onPathActivated = options.onPathActivated;
64
85
  this.addClass(BREADCRUMB_CLASS);
65
86
  this._crumbs = Private.createCrumbs();
66
87
  const hasPreferred = PageConfig.getOption('preferredPath');
67
88
  this._hasPreferred = hasPreferred && hasPreferred !== '/' ? true : false;
68
- if (this._hasPreferred) {
69
- this.node.appendChild(this._crumbs[Private.Crumb.Preferred]);
70
- }
71
- this.node.appendChild(this._crumbs[Private.Crumb.Home]);
72
- this._model.refreshed.connect(this.update, this);
89
+ this._crumbContainer = document.createElement('span');
90
+ this._crumbContainer.className = BREADCRUMB_CONTAINER_CLASS;
91
+ this._crumbContent = document.createElement('span');
92
+ this._crumbContent.className = BREADCRUMB_CONTENT_CLASS;
93
+ this._crumbContainer.appendChild(this._crumbContent);
94
+ this.node.appendChild(this._crumbContainer);
95
+ this._model.refreshed.connect(this._onModelRefreshed, this);
73
96
  this._resizeThrottler = new Throttler(() => this._onResize(), 50);
74
97
  this._resizeObserver = new ResizeObserver(entries => {
75
98
  const entry = entries[0];
@@ -84,6 +107,11 @@ export class BreadCrumbs extends Widget {
84
107
  void this._resizeThrottler.invoke();
85
108
  }
86
109
  });
110
+ this._pathNavigator = new PathNavigator({
111
+ model: this._model,
112
+ translator: options.translator
113
+ });
114
+ this._pathNavigator.closed.connect(this._exitEditMode, this);
87
115
  }
88
116
  /**
89
117
  * Handle the DOM events for the bread crumbs.
@@ -97,6 +125,9 @@ export class BreadCrumbs extends Widget {
97
125
  */
98
126
  handleEvent(event) {
99
127
  switch (event.type) {
128
+ case 'keydown':
129
+ this._evtKeyDown(event);
130
+ break;
100
131
  case 'click':
101
132
  this._evtClick(event);
102
133
  break;
@@ -150,6 +181,9 @@ export class BreadCrumbs extends Widget {
150
181
  if (this.isDisposed) {
151
182
  return;
152
183
  }
184
+ this._model.refreshed.disconnect(this._onModelRefreshed, this);
185
+ this._pathNavigator.closed.disconnect(this._exitEditMode, this);
186
+ this._pathNavigator.dispose();
153
187
  this._resizeObserver.disconnect();
154
188
  this._resizeThrottler.dispose();
155
189
  super.dispose();
@@ -161,19 +195,23 @@ export class BreadCrumbs extends Widget {
161
195
  super.onAfterAttach(msg);
162
196
  this.update();
163
197
  const node = this.node;
198
+ node.addEventListener('keydown', this);
164
199
  node.addEventListener('click', this);
165
200
  node.addEventListener('lm-dragenter', this);
166
201
  node.addEventListener('lm-dragleave', this);
167
202
  node.addEventListener('lm-dragover', this);
168
203
  this._resizeObserver.observe(node);
169
204
  node.addEventListener('lm-drop', this);
205
+ Widget.attach(this._pathNavigator, this.node);
170
206
  }
171
207
  /**
172
208
  * A message handler invoked on a `'before-detach'` message.
173
209
  */
174
210
  onBeforeDetach(msg) {
211
+ Widget.detach(this._pathNavigator);
175
212
  super.onBeforeDetach(msg);
176
213
  const node = this.node;
214
+ node.removeEventListener('keydown', this);
177
215
  node.removeEventListener('click', this);
178
216
  node.removeEventListener('lm-dragenter', this);
179
217
  node.removeEventListener('lm-dragleave', this);
@@ -185,9 +223,18 @@ export class BreadCrumbs extends Widget {
185
223
  * A handler invoked on an `'update-request'` message.
186
224
  */
187
225
  onUpdateRequest(msg) {
226
+ if (this._isEditMode) {
227
+ // In edit mode the CSS class hides breadcrumb content,
228
+ // we do not need to refresh, as only the pathnavigator is visible.
229
+ return;
230
+ }
188
231
  // Update the breadcrumb list.
189
232
  const contents = this._model.manager.services.contents;
190
233
  const localPath = contents.localPath(this._model.path);
234
+ // We track the path on which we are;
235
+ // this is to make sure we don't exit edit mode on model refresh if
236
+ // the path has not changed.
237
+ this._lastPath = localPath;
191
238
  // Invalidate cached widths if the path changed
192
239
  if (this._previousState && this._previousState.path !== localPath) {
193
240
  this._cachedWidths = null;
@@ -196,16 +243,191 @@ export class BreadCrumbs extends Widget {
196
243
  const adaptiveItems = this._calculateAdaptiveItems(localPath);
197
244
  const state = {
198
245
  path: localPath,
246
+ root: this._model.root,
199
247
  hasPreferred: this._hasPreferred,
200
248
  fullPath: this._fullPath,
201
249
  minimumLeftItems: adaptiveItems.left,
202
250
  minimumRightItems: adaptiveItems.right
203
251
  };
204
- if (this._previousState && JSONExt.deepEqual(state, this._previousState)) {
252
+ const stateUnchanged = this._previousState !== null &&
253
+ JSONExt.deepEqual(state, this._previousState);
254
+ if (!stateUnchanged) {
255
+ this._previousState = state;
256
+ Private.updateCrumbs(this._crumbContent, this._crumbs, state);
257
+ this._syncCrumbTabIndices();
258
+ }
259
+ this._runPostActivationFocus();
260
+ }
261
+ /**
262
+ * Restore crumb focus after breadcrumb activation and invoke activation callback.
263
+ */
264
+ _runPostActivationFocus() {
265
+ if (!this._restoreBreadcrumbFocusAfterUpdate) {
266
+ return;
267
+ }
268
+ this._restoreBreadcrumbFocusAfterUpdate = false;
269
+ requestAnimationFrame(() => {
270
+ var _a;
271
+ if (this.isDisposed || this._isEditMode) {
272
+ return;
273
+ }
274
+ (_a = this._onPathActivated) === null || _a === void 0 ? void 0 : _a.call(this);
275
+ });
276
+ }
277
+ /**
278
+ * Return breadcrumb segments in DOM order that participate in arrow-key focus.
279
+ */
280
+ _getFocusableCrumbElements() {
281
+ const result = [];
282
+ const children = this._crumbContent.children;
283
+ for (let i = 0; i < children.length; i++) {
284
+ const el = children[i];
285
+ if (el.classList.contains(BREADCRUMB_PREFERRED_CLASS) ||
286
+ el.classList.contains(BREADCRUMB_ROOT_CLASS) ||
287
+ el.classList.contains(BREADCRUMB_ITEM_CLASS)) {
288
+ result.push(el);
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+ /**
294
+ * Roving tabindex: one segment is in tab order (`tabIndex` 0), the rest -1.
295
+ */
296
+ _syncCrumbTabIndices() {
297
+ const items = this._getFocusableCrumbElements();
298
+ for (let i = 0; i < items.length; i++) {
299
+ items[i].tabIndex = i === 0 ? 0 : -1;
300
+ }
301
+ }
302
+ /**
303
+ * Focus a crumb segment and make it the sole tab stop within the trail.
304
+ */
305
+ _focusCrumb(crumb) {
306
+ const items = this._getFocusableCrumbElements();
307
+ const idx = items.indexOf(crumb);
308
+ if (idx === -1) {
309
+ return;
310
+ }
311
+ for (let i = 0; i < items.length; i++) {
312
+ items[i].tabIndex = i === idx ? 0 : -1;
313
+ }
314
+ crumb.focus();
315
+ }
316
+ /**
317
+ * Focus the last segment in the trail (the current directory), for keyboard UX after navigation.
318
+ */
319
+ _focusTrailingCrumb() {
320
+ const items = this._getFocusableCrumbElements();
321
+ if (items.length === 0) {
322
+ // If there is no focusable crumb segment (e.g. root-restricted path),
323
+ // keep focus inside the breadcrumb widget itself.
324
+ this.node.tabIndex = -1;
325
+ this.node.focus();
326
+ return;
327
+ }
328
+ this._focusCrumb(items[items.length - 1]);
329
+ }
330
+ /**
331
+ * Walk from an event target to the nearest breadcrumb segment host, if any.
332
+ */
333
+ _resolveCrumbFromEventTarget(target) {
334
+ let node = target;
335
+ while (node && node !== this.node) {
336
+ if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS) ||
337
+ node.classList.contains(BREADCRUMB_ROOT_CLASS) ||
338
+ node.classList.contains(BREADCRUMB_ITEM_CLASS)) {
339
+ return node;
340
+ }
341
+ node = node.parentElement;
342
+ }
343
+ return null;
344
+ }
345
+ /**
346
+ * Whether a crumb corresponds to the current directory segment.
347
+ */
348
+ _isCurrentDirectoryCrumb(crumb) {
349
+ if (!crumb.classList.contains(BREADCRUMB_ITEM_CLASS) ||
350
+ crumb.classList.contains(BREADCRUMB_ELLIPSIS_CLASS)) {
351
+ return false;
352
+ }
353
+ const contents = this._model.manager.services.contents;
354
+ const localPath = contents.localPath(this._model.path);
355
+ return crumb.dataset.path === localPath;
356
+ }
357
+ /**
358
+ * Navigate to the directory represented by a breadcrumb segment.
359
+ */
360
+ _activateCrumbSegment(crumb) {
361
+ this._focusCrumb(crumb);
362
+ const destination = this._destinationForCrumb(crumb);
363
+ if (!destination) {
364
+ return;
365
+ }
366
+ this._restoreBreadcrumbFocusAfterUpdate = true;
367
+ this._model.cd(destination).catch(error => {
368
+ this._restoreBreadcrumbFocusAfterUpdate = false;
369
+ void showErrorMessage(this._trans.__('Open Error'), error);
370
+ });
371
+ }
372
+ /**
373
+ * Resolve the destination path for an activated breadcrumb segment.
374
+ */
375
+ _destinationForCrumb(crumb) {
376
+ if (crumb.classList.contains(BREADCRUMB_PREFERRED_CLASS)) {
377
+ const preferredPath = PageConfig.getOption('preferredPath');
378
+ return preferredPath ? '/' + preferredPath : '/';
379
+ }
380
+ if (crumb.classList.contains(BREADCRUMB_ROOT_CLASS)) {
381
+ return '/';
382
+ }
383
+ if (crumb.classList.contains(BREADCRUMB_ITEM_CLASS)) {
384
+ return `/${crumb.dataset.path}`;
385
+ }
386
+ return null;
387
+ }
388
+ /**
389
+ * Handle the `'keydown'` event for the widget.
390
+ */
391
+ _evtKeyDown(event) {
392
+ if (this._isEditMode) {
205
393
  return;
206
394
  }
207
- this._previousState = state;
208
- Private.updateCrumbs(this._crumbs, state);
395
+ const isActivateKey = event.key === 'Enter' || event.key === ' ';
396
+ const crumb = this._resolveCrumbFromEventTarget(event.target);
397
+ const localPath = this._model.manager.services.contents.localPath(this._model.path);
398
+ if (isActivateKey && crumb && this._crumbContent.contains(crumb)) {
399
+ const enterEditModeFromRoot = event.key === ' ' &&
400
+ crumb.classList.contains(BREADCRUMB_ROOT_CLASS) &&
401
+ localPath === '';
402
+ if (this._isCurrentDirectoryCrumb(crumb) || enterEditModeFromRoot) {
403
+ this.enterEditMode();
404
+ }
405
+ else {
406
+ this._activateCrumbSegment(crumb);
407
+ }
408
+ event.preventDefault();
409
+ event.stopPropagation();
410
+ return;
411
+ }
412
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
413
+ return;
414
+ }
415
+ if (!crumb || !this._crumbContent.contains(crumb)) {
416
+ return;
417
+ }
418
+ const items = this._getFocusableCrumbElements();
419
+ const idx = items.indexOf(crumb);
420
+ if (idx === -1) {
421
+ return;
422
+ }
423
+ const delta = event.key === 'ArrowLeft' ? -1 : 1;
424
+ const nextIdx = idx + delta;
425
+ if (nextIdx < 0 || nextIdx >= items.length) {
426
+ return;
427
+ }
428
+ this._focusCrumb(items[nextIdx]);
429
+ event.preventDefault();
430
+ event.stopPropagation();
209
431
  }
210
432
  /**
211
433
  * Handle the `'click'` event for the widget.
@@ -215,34 +437,17 @@ export class BreadCrumbs extends Widget {
215
437
  if (event.button !== 0) {
216
438
  return;
217
439
  }
440
+ // In edit mode the PathNavigator owns all interaction.
441
+ if (this._isEditMode) {
442
+ return;
443
+ }
218
444
  // Find a valid click target.
219
445
  let node = event.target;
220
446
  while (node && node !== this.node) {
221
- if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS)) {
222
- const preferredPath = PageConfig.getOption('preferredPath');
223
- const path = preferredPath ? '/' + preferredPath : preferredPath;
224
- this._model
225
- .cd(path)
226
- .catch(error => showErrorMessage(this._trans.__('Open Error'), error));
227
- // Stop the event propagation.
228
- event.preventDefault();
229
- event.stopPropagation();
230
- return;
231
- }
232
- if (node.classList.contains(BREADCRUMB_ITEM_CLASS) ||
447
+ if (node.classList.contains(BREADCRUMB_PREFERRED_CLASS) ||
448
+ node.classList.contains(BREADCRUMB_ITEM_CLASS) ||
233
449
  node.classList.contains(BREADCRUMB_ROOT_CLASS)) {
234
- let destination;
235
- if (node.classList.contains(BREADCRUMB_ROOT_CLASS)) {
236
- destination = '/';
237
- }
238
- else {
239
- destination = `/${node.dataset.path}`;
240
- }
241
- if (destination) {
242
- this._model
243
- .cd(destination)
244
- .catch(error => showErrorMessage(this._trans.__('Open Error'), error));
245
- }
450
+ this._activateCrumbSegment(node);
246
451
  // Stop the event propagation.
247
452
  event.preventDefault();
248
453
  event.stopPropagation();
@@ -250,6 +455,10 @@ export class BreadCrumbs extends Widget {
250
455
  }
251
456
  node = node.parentElement;
252
457
  }
458
+ // Click landed on the breadcrumb background (including separators
459
+ // between crumb items) — enter edit mode. This is intentional: the
460
+ // entire breadcrumb bar acts as a click target for the path editor.
461
+ this.enterEditMode();
253
462
  }
254
463
  /**
255
464
  * Handle the `'lm-dragenter'` event for the widget.
@@ -369,7 +578,7 @@ export class BreadCrumbs extends Widget {
369
578
  */
370
579
  _getBreadcrumbElements() {
371
580
  const elements = [];
372
- const children = this.node.children;
581
+ const children = this._crumbContent.children;
373
582
  for (let i = 0; i < children.length; i++) {
374
583
  const child = children[i];
375
584
  if ((child.classList.contains(BREADCRUMB_ITEM_CLASS) ||
@@ -398,7 +607,7 @@ export class BreadCrumbs extends Widget {
398
607
  * including those currently hidden behind the ellipsis.
399
608
  */
400
609
  _measureAllItemWidths(parts) {
401
- const node = this.node;
610
+ const node = this._crumbContent;
402
611
  // Measure fixed elements that are already in the DOM
403
612
  const home = this._crumbs[Private.Crumb.Home];
404
613
  const ellipsis = this._crumbs[Private.Crumb.Ellipsis];
@@ -472,7 +681,7 @@ export class BreadCrumbs extends Widget {
472
681
  // Calculate available space for items
473
682
  let fixedOverhead = homeWidth + separatorWidth;
474
683
  if (this._hasPreferred) {
475
- fixedOverhead += preferredWidth + separatorWidth;
684
+ fixedOverhead += preferredWidth;
476
685
  }
477
686
  const availableForItems = containerWidth - fixedOverhead;
478
687
  // Check if all parts can fit without ellipsis
@@ -524,6 +733,63 @@ export class BreadCrumbs extends Widget {
524
733
  right: finalRight
525
734
  };
526
735
  }
736
+ /**
737
+ * Enter edit mode: show the path input and hide the breadcrumb content.
738
+ */
739
+ enterEditMode() {
740
+ this._isEditMode = true;
741
+ // Snapshot the current path so _onModelRefreshed can reliably detect
742
+ // whether the path actually changed while in edit mode.
743
+ const contents = this._model.manager.services.contents;
744
+ this._lastPath = contents.localPath(this._model.path);
745
+ this.node.classList.add(BREADCRUMB_EDIT_MODE_CLASS);
746
+ // Clear cached state so that when we exit edit mode, onUpdateRequest
747
+ // will unconditionally re-render the breadcrumbs (the path may have
748
+ // changed while we were editing). _exitEditMode also clears this for
749
+ // the same reason — the double-null is intentional even if technically
750
+ // unnecessary.
751
+ this._previousState = null;
752
+ this._pathNavigator.open();
753
+ }
754
+ /**
755
+ * Move focus to the trailing breadcrumb segment.
756
+ */
757
+ focusLastCrumb() {
758
+ // Defer to ensure any pending breadcrumb DOM updates are applied.
759
+ requestAnimationFrame(() => {
760
+ this._focusTrailingCrumb();
761
+ });
762
+ }
763
+ /**
764
+ * Exit edit mode and restore the breadcrumb display.
765
+ */
766
+ _exitEditMode() {
767
+ if (!this._isEditMode) {
768
+ return;
769
+ }
770
+ this._isEditMode = false;
771
+ this.node.classList.remove(BREADCRUMB_EDIT_MODE_CLASS);
772
+ this._previousState = null;
773
+ this.update();
774
+ }
775
+ /**
776
+ * Handle the model's `refreshed` signal.
777
+ * If we are in edit mode, dismiss it (the model path may have changed).
778
+ */
779
+ _onModelRefreshed() {
780
+ var _a;
781
+ const contents = this._model.manager.services.contents;
782
+ const localPath = contents.localPath(this._model.path);
783
+ if (this._isEditMode) {
784
+ if (this._pathNavigator.matchesSubmittedPath(localPath) ||
785
+ localPath !== this._lastPath) {
786
+ this._exitEditMode();
787
+ (_a = this._onPathEdited) === null || _a === void 0 ? void 0 : _a.call(this);
788
+ }
789
+ return;
790
+ }
791
+ this.update();
792
+ }
527
793
  }
528
794
  /**
529
795
  * The namespace for the crumbs private data.
@@ -540,73 +806,84 @@ var Private;
540
806
  Crumb[Crumb["Preferred"] = 2] = "Preferred";
541
807
  })(Crumb = Private.Crumb || (Private.Crumb = {}));
542
808
  /**
543
- * Populate the breadcrumb node.
809
+ * Populate the breadcrumb container node.
810
+ *
811
+ * @param container - The container element that holds breadcrumb items.
812
+ * @param breadcrumbs - The reusable breadcrumb elements (Home, Ellipsis, Preferred).
813
+ * @param state - The current breadcrumb state.
544
814
  */
545
- function updateCrumbs(breadcrumbs, state) {
546
- const node = breadcrumbs[0].parentNode;
547
- // Remove all but the home or preferred node.
548
- const firstChild = node.firstChild;
549
- while (firstChild && firstChild.nextSibling) {
550
- node.removeChild(firstChild.nextSibling);
551
- }
552
- if (state.hasPreferred) {
553
- node.appendChild(breadcrumbs[Crumb.Home]);
554
- node.appendChild(createCrumbSeparator());
815
+ function updateCrumbs(container, breadcrumbs, state) {
816
+ const nodes = [];
817
+ // Calculate the starting index for breadcrumbs based on root restriction
818
+ const rootParts = state.root
819
+ ? state.root.split('/').filter(part => part !== '')
820
+ : [];
821
+ const rootDepth = rootParts.length;
822
+ // Only show home/preferred if there's no root restriction
823
+ if (!state.root) {
824
+ if (state.hasPreferred) {
825
+ nodes.push(breadcrumbs[Private.Crumb.Preferred]);
826
+ }
827
+ nodes.push(breadcrumbs[Crumb.Home], createCrumbSeparator());
555
828
  }
556
829
  else {
557
- node.appendChild(createCrumbSeparator());
830
+ // With root restriction, just add an initial separator
831
+ nodes.push(createCrumbSeparator());
558
832
  }
559
833
  const parts = state.path.split('/').filter(part => part !== '');
560
- if (!state.fullPath && parts.length > 0) {
834
+ // Filter parts to only show those at or below the root
835
+ const visibleParts = state.root ? parts.slice(rootDepth) : parts;
836
+ const visibleStartIndex = rootDepth;
837
+ if (!state.fullPath && visibleParts.length > 0) {
561
838
  const minimumLeftItems = state.minimumLeftItems;
562
839
  const minimumRightItems = state.minimumRightItems;
563
- // Check if we need ellipsis
564
- if (parts.length > minimumLeftItems + minimumRightItems) {
840
+ // Check if we need ellipsis (based on visible parts)
841
+ if (visibleParts.length > minimumLeftItems + minimumRightItems) {
565
842
  // Add left items
566
843
  for (let i = 0; i < minimumLeftItems; i++) {
567
- const elemPath = parts.slice(0, i + 1).join('/');
568
- const elem = createBreadcrumbElement(parts[i], elemPath);
569
- node.appendChild(elem);
570
- node.appendChild(createCrumbSeparator());
844
+ const fullIndex = visibleStartIndex + i;
845
+ const elemPath = parts.slice(0, fullIndex + 1).join('/');
846
+ nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
847
+ nodes.push(createCrumbSeparator());
571
848
  }
572
849
  // Add ellipsis
573
- node.appendChild(breadcrumbs[Crumb.Ellipsis]);
574
850
  const hiddenStartIndex = minimumLeftItems;
575
- const hiddenEndIndex = parts.length - minimumRightItems;
576
- const hiddenParts = parts.slice(hiddenStartIndex, hiddenEndIndex);
577
- const hiddenFolders = hiddenParts.join('/');
578
- const hiddenPath = hiddenParts.length > 0
579
- ? parts.slice(0, hiddenEndIndex).join('/')
580
- : parts.slice(0, minimumLeftItems).join('/');
581
- breadcrumbs[Crumb.Ellipsis].title = hiddenFolders;
582
- breadcrumbs[Crumb.Ellipsis].dataset.path = hiddenPath;
583
- node.appendChild(createCrumbSeparator());
851
+ const hiddenEndIndex = visibleParts.length - minimumRightItems;
852
+ const hiddenVisibleParts = visibleParts.slice(hiddenStartIndex, hiddenEndIndex);
853
+ const ellipsis = breadcrumbs[Crumb.Ellipsis];
854
+ ellipsis.title = hiddenVisibleParts.join('/');
855
+ ellipsis.dataset.path =
856
+ hiddenVisibleParts.length > 0
857
+ ? parts.slice(0, visibleStartIndex + hiddenEndIndex).join('/')
858
+ : parts.slice(0, visibleStartIndex + minimumLeftItems).join('/');
859
+ nodes.push(ellipsis, createCrumbSeparator());
584
860
  // Add right items
585
- const rightStartIndex = parts.length - minimumRightItems;
586
- for (let i = rightStartIndex; i < parts.length; i++) {
587
- const elemPath = parts.slice(0, i + 1).join('/');
588
- const elem = createBreadcrumbElement(parts[i], elemPath);
589
- node.appendChild(elem);
590
- node.appendChild(createCrumbSeparator());
861
+ const rightStartIndex = visibleParts.length - minimumRightItems;
862
+ for (let i = rightStartIndex; i < visibleParts.length; i++) {
863
+ const fullIndex = visibleStartIndex + i;
864
+ const elemPath = parts.slice(0, fullIndex + 1).join('/');
865
+ nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
866
+ nodes.push(createCrumbSeparator());
591
867
  }
592
868
  }
593
869
  else {
594
- for (let i = 0; i < parts.length; i++) {
595
- const elemPath = parts.slice(0, i + 1).join('/');
596
- const elem = createBreadcrumbElement(parts[i], elemPath);
597
- node.appendChild(elem);
598
- node.appendChild(createCrumbSeparator());
870
+ for (let i = 0; i < visibleParts.length; i++) {
871
+ const fullIndex = visibleStartIndex + i;
872
+ const elemPath = parts.slice(0, fullIndex + 1).join('/');
873
+ nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
874
+ nodes.push(createCrumbSeparator());
599
875
  }
600
876
  }
601
877
  }
602
- else if (state.fullPath && parts.length > 0) {
603
- for (let i = 0; i < parts.length; i++) {
604
- const elemPath = parts.slice(0, i + 1).join('/');
605
- const elem = createBreadcrumbElement(parts[i], elemPath);
606
- node.appendChild(elem);
607
- node.appendChild(createCrumbSeparator());
878
+ else if (state.fullPath && visibleParts.length > 0) {
879
+ for (let i = 0; i < visibleParts.length; i++) {
880
+ const fullIndex = visibleStartIndex + i;
881
+ const elemPath = parts.slice(0, fullIndex + 1).join('/');
882
+ nodes.push(createBreadcrumbElement(visibleParts[i], elemPath));
883
+ nodes.push(createCrumbSeparator());
608
884
  }
609
885
  }
886
+ container.replaceChildren(...nodes);
610
887
  }
611
888
  Private.updateCrumbs = updateCrumbs;
612
889
  /**
@@ -618,6 +895,7 @@ var Private;
618
895
  elem.textContent = pathPart;
619
896
  elem.title = fullPath;
620
897
  elem.dataset.path = fullPath;
898
+ elem.tabIndex = -1;
621
899
  return elem;
622
900
  }
623
901
  /**
@@ -631,11 +909,13 @@ var Private;
631
909
  stylesheet: 'breadCrumb'
632
910
  });
633
911
  home.dataset.path = '/';
912
+ home.tabIndex = -1;
634
913
  const ellipsis = ellipsesIcon.element({
635
914
  className: `${BREADCRUMB_ITEM_CLASS} ${BREADCRUMB_ELLIPSIS_CLASS}`,
636
915
  tag: 'span',
637
916
  stylesheet: 'breadCrumb'
638
917
  });
918
+ ellipsis.tabIndex = -1;
639
919
  const preferredPath = PageConfig.getOption('preferredPath');
640
920
  const path = preferredPath ? '/' + preferredPath : preferredPath;
641
921
  const preferred = preferredIcon.element({
@@ -645,6 +925,7 @@ var Private;
645
925
  stylesheet: 'breadCrumb'
646
926
  });
647
927
  preferred.dataset.path = path || '/';
928
+ preferred.tabIndex = -1;
648
929
  return [home, ellipsis, preferred];
649
930
  }
650
931
  Private.createCrumbs = createCrumbs;