From 30f71e88c4d37b137c76a58c6d0b8daa9e8bde17 Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Sun, 27 Mar 2022 23:31:00 +0100
Subject: [PATCH 1/8] Implement scrolling of tabs in tabbar

---
 packages/widgets/src/dockpanel.ts |  29 +++
 packages/widgets/src/tabbar.ts    | 307 +++++++++++++++++++++++++++---
 packages/widgets/style/tabbar.css | 128 ++++++++++++-
 3 files changed, 440 insertions(+), 24 deletions(-)

diff --git a/packages/widgets/src/dockpanel.ts b/packages/widgets/src/dockpanel.ts
index ed045d96d..28a1444de 100644
--- a/packages/widgets/src/dockpanel.ts
+++ b/packages/widgets/src/dockpanel.ts
@@ -54,6 +54,9 @@ export class DockPanel extends Widget {
     if (options.addButtonEnabled !== undefined) {
       this._addButtonEnabled = options.addButtonEnabled;
     }
+    if (options.tabScrollingEnabled !== undefined) {
+      this._tabScrollingEnabled = options.tabScrollingEnabled;
+    }
 
     // Toggle the CSS mode attribute.
     this.dataset['mode'] = this._mode;
@@ -255,6 +258,23 @@ export class DockPanel extends Widget {
     });
   }
 
+  /**
+   * Whether scrolling of tabs in tab bars is enabled.
+   */
+  get tabScrollingEnabled(): boolean {
+    return this._tabScrollingEnabled;
+  }
+
+  /**
+   * Set whether the add buttons for each tab bar are enabled.
+   */
+  set tabScrollingEnabled(value: boolean) {
+    this._tabScrollingEnabled = value;
+    each(this.tabBars(), tabbar => {
+      tabbar.scrollingEnabled = value;
+    });
+  }
+
   /**
    * Whether the dock panel is empty.
    */
@@ -914,6 +934,7 @@ export class DockPanel extends Widget {
     tabBar.tabsMovable = this._tabsMovable;
     tabBar.allowDeselect = false;
     tabBar.addButtonEnabled = this._addButtonEnabled;
+    tabBar.scrollingEnabled = this._tabScrollingEnabled;
     tabBar.removeBehavior = 'select-previous-tab';
     tabBar.insertBehavior = 'select-tab-if-needed';
 
@@ -1054,6 +1075,7 @@ export class DockPanel extends Widget {
   private _tabsMovable: boolean = true;
   private _tabsConstrained: boolean = false;
   private _addButtonEnabled: boolean = false;
+  private _tabScrollingEnabled: boolean = false;
   private _pressData: Private.IPressData | null = null;
   private _layoutModified = new Signal<this, void>(this);
 
@@ -1136,6 +1158,13 @@ export namespace DockPanel {
      * The default is `'false'`.
      */
     addButtonEnabled?: boolean;
+
+    /**
+     * Enable scrolling in each of the dock panel's tab bars.
+     *
+     * The default is `'false'`.
+     */
+    tabScrollingEnabled?: boolean;
   }
 
   /**
diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index 9f0e925a4..bb66dcbf4 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -350,6 +350,28 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  /**
+   * Whether scrolling is enabled.
+   */
+  get scrollingEnabled(): boolean {
+    return this._scrollingEnabled;
+  }
+
+  set scrollingEnabled(value: boolean) {
+    // Do nothing if the value does not change.
+    if (this._scrollingEnabled === value) {
+      return;
+    }
+
+    this._scrollingEnabled = value;
+    if (value) {
+      this.node.classList.add('lm-mod-scrollable');
+    } else {
+      this.node.classList.add('lm-mod-scrollable');
+    }
+    this.maybeSwitchScrollButtons();
+  }
+
   /**
    * A read-only array of the titles in the tab bar.
    */
@@ -371,6 +393,20 @@ export class TabBar<T> extends Widget {
     )[0] as HTMLUListElement;
   }
 
+  /**
+   * The tab bar content wrapper node.
+   *
+   * #### Notes
+   * This is the node which the content node and enables scrolling.
+   *
+   * Modifying this node directly can lead to undefined behavior.
+   */
+  get contentWrapperNode(): HTMLUListElement {
+    return this.node.getElementsByClassName(
+      'lm-TabBar-wrapper'
+    )[0] as HTMLUListElement;
+  }
+
   /**
    * The tab bar add button node.
    *
@@ -385,6 +421,18 @@ export class TabBar<T> extends Widget {
     )[0] as HTMLDivElement;
   }
 
+  get scrollBeforeButtonNode(): HTMLDivElement {
+    return this.node.getElementsByClassName(
+      'lm-TabBar-scrollBeforeButton'
+    )[0] as HTMLDivElement;
+  }
+
+  get scrollAfterButtonNode(): HTMLDivElement {
+    return this.node.getElementsByClassName(
+      'lm-TabBar-scrollAfterButton'
+    )[0] as HTMLDivElement;
+  }
+
   /**
    * Add a tab to the end of the tab bar.
    *
@@ -606,6 +654,9 @@ export class TabBar<T> extends Widget {
         event.preventDefault();
         event.stopPropagation();
         break;
+      case 'scroll':
+        this._evtScroll(event);
+        break;
     }
   }
 
@@ -615,6 +666,7 @@ export class TabBar<T> extends Widget {
   protected onBeforeAttach(msg: Message): void {
     this.node.addEventListener('pointerdown', this);
     this.node.addEventListener('dblclick', this);
+    this.contentNode.addEventListener('scroll', this);
   }
 
   /**
@@ -623,6 +675,7 @@ export class TabBar<T> extends Widget {
   protected onAfterDetach(msg: Message): void {
     this.node.removeEventListener('pointerdown', this);
     this.node.removeEventListener('dblclick', this);
+    this.contentNode.removeEventListener('scroll', this);
     this._releaseMouse();
   }
 
@@ -641,6 +694,80 @@ export class TabBar<T> extends Widget {
       content[i] = renderer.renderTab({ title, current, zIndex });
     }
     VirtualDOM.render(content, this.contentNode);
+    this.maybeSwitchScrollButtons();
+  }
+
+  protected onResize(msg: Widget.ResizeMessage): void {
+    super.onResize(msg);
+    this.maybeSwitchScrollButtons();
+  }
+
+  protected maybeSwitchScrollButtons() {
+    const scrollBefore = this.scrollBeforeButtonNode;
+    const scrollAfter = this.scrollAfterButtonNode;
+    const state = this._scrollState;
+
+    if (this.scrollingEnabled && state.totalSize > state.displayedSize) {
+      // show both buttons
+      scrollBefore.classList.remove('lm-mod-hidden');
+      scrollAfter.classList.remove('lm-mod-hidden');
+    } else {
+      // hide both buttons
+      scrollBefore.classList.add('lm-mod-hidden');
+      scrollAfter.classList.add('lm-mod-hidden');
+    }
+    this.updateScrollingHints(state);
+  }
+
+  /**
+   * Adjust data reflecting the ability to scroll in each direction.
+   */
+  protected updateScrollingHints(scrollState: Private.IScrollState) {
+    const wrapper = this.contentWrapperNode;
+
+    if (!this.scrollingEnabled) {
+      delete wrapper.dataset['canScroll'];
+      return;
+    }
+
+    const canScrollBefore = scrollState.position != 0;
+    const canScrollAfter =
+      scrollState.position != scrollState.totalSize - scrollState.displayedSize;
+
+    if (canScrollBefore && canScrollAfter) {
+      wrapper.dataset['canScroll'] = 'both';
+    } else if (canScrollBefore) {
+      wrapper.dataset['canScroll'] = 'before';
+    } else if (canScrollAfter) {
+      wrapper.dataset['canScroll'] = 'after';
+    } else {
+      delete wrapper.dataset['canScroll'];
+    }
+  }
+
+  private get _scrollState(): Private.IScrollState {
+    const content = this.contentNode;
+    const contentRect = content.getBoundingClientRect();
+    const isHorizontal = this.orientation === 'horizontal';
+    const contentSize = isHorizontal ? contentRect.width : contentRect.height;
+    const scrollTotal = isHorizontal
+      ? content.scrollWidth
+      : content.scrollHeight;
+    const scroll = Math.round(
+      isHorizontal ? content.scrollLeft : content.scrollTop
+    );
+    return {
+      displayedSize: Math.round(contentSize),
+      totalSize: scrollTotal,
+      position: scroll
+    };
+  }
+
+  /**
+   * Handle the `'dblclick'` event for the tab bar.
+   */
+  private _evtScroll(event: Event): void {
+    this.updateScrollingHints(this._scrollState);
   }
 
   /**
@@ -720,6 +847,52 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  protected beginScrolling(direction: '-' | '+') {
+    const initialRate = 5;
+    const rateIncrease = 1;
+    const maxRate = 20;
+    const intervalHandle = setInterval(() => {
+      if (!this._scrollData) {
+        this.stopScrolling();
+        return;
+      }
+      const rate = this._scrollData.rate;
+      const direction = this._scrollData.scrollDirection;
+      const change = (direction == '+' ? 1 : -1) * rate;
+      if (this.orientation == 'horizontal') {
+        this.contentNode.scrollLeft += change;
+      } else {
+        this.contentNode.scrollTop += change;
+      }
+      this._scrollData.rate = Math.min(
+        this._scrollData.rate + rateIncrease,
+        maxRate
+      );
+      const state = this._scrollState;
+      if (
+        (direction == '-' && state.position == 0) ||
+        (direction == '+' &&
+          state.totalSize == state.position + state.displayedSize)
+      ) {
+        this.stopScrolling();
+      }
+    }, 50);
+    this._scrollData = {
+      timerHandle: intervalHandle,
+      scrollDirection: direction,
+      rate: initialRate
+    };
+  }
+
+  protected stopScrolling() {
+    if (this._scrollData) {
+      clearInterval(this._scrollData.timerHandle);
+    }
+    this._scrollData = null;
+    const state = this._scrollState;
+    this.updateScrollingHints(state);
+  }
+
   /**
    * Handle the `'pointerdown'` event for the tab bar.
    */
@@ -739,6 +912,19 @@ export class TabBar<T> extends Widget {
       this.addButtonEnabled &&
       this.addButtonNode.contains(event.target as HTMLElement);
 
+    let scrollBeforeButtonClicked =
+      this.scrollingEnabled &&
+      this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
+
+    let scrollAfterButtonClicked =
+      this.scrollingEnabled &&
+      this.scrollAfterButtonNode.contains(event.target as HTMLElement);
+
+    const anyButtonClicked =
+      addButtonClicked || scrollAfterButtonClicked || scrollBeforeButtonClicked;
+
+    const contentNode = this.contentNode;
+
     // Lookup the tab nodes.
     let tabs = this.contentNode.children;
 
@@ -747,8 +933,8 @@ export class TabBar<T> extends Widget {
       return ElementExt.hitTest(tab, event.clientX, event.clientY);
     });
 
-    // Do nothing if the press is not on a tab or the add button.
-    if (index === -1 && !addButtonClicked) {
+    // Do nothing if the press is not on a tab or any of the buttons.
+    if (index === -1 && !anyButtonClicked) {
       return;
     }
 
@@ -762,6 +948,10 @@ export class TabBar<T> extends Widget {
       index: index,
       pressX: event.clientX,
       pressY: event.clientY,
+      initialScrollPosition:
+        this.orientation == 'horizontal'
+          ? contentNode.scrollLeft
+          : contentNode.scrollTop,
       tabPos: -1,
       tabSize: -1,
       tabPressPos: -1,
@@ -781,6 +971,10 @@ export class TabBar<T> extends Widget {
     if (event.button === 1 || addButtonClicked) {
       return;
     }
+    if (scrollBeforeButtonClicked || scrollAfterButtonClicked) {
+      this.beginScrolling(scrollBeforeButtonClicked ? '-' : '+');
+      return;
+    }
 
     // Do nothing else if the close icon is clicked.
     let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
@@ -882,8 +1076,24 @@ export class TabBar<T> extends Widget {
       }
     }
 
+    let overBeforeScrollButton =
+      this.scrollingEnabled &&
+      this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
+
+    let overAfterScrollButton =
+      this.scrollingEnabled &&
+      this.scrollAfterButtonNode.contains(event.target as HTMLElement);
+
+    if (overBeforeScrollButton || overAfterScrollButton) {
+      // Start scrolling if the mouse is over scroll buttons
+      this.beginScrolling(overBeforeScrollButton ? '-' : '+');
+    } else {
+      // Stop scrolling if mouse is not over scroll buttons
+      this.stopScrolling();
+    }
+
     // Update the positions of the tabs.
-    Private.layoutTabs(tabs, data, event, this._orientation);
+    Private.layoutTabs(tabs, data, event, this._orientation, this._scrollState);
   }
 
   /**
@@ -916,6 +1126,10 @@ export class TabBar<T> extends Widget {
       // Clear the drag data.
       this._dragData = null;
 
+      if (this._scrollData) {
+        this.stopScrolling();
+        return;
+      }
       // Handle clicking the add button.
       let addButtonClicked =
         this.addButtonEnabled &&
@@ -967,7 +1181,7 @@ export class TabBar<T> extends Widget {
     }
 
     // Position the tab at its final resting position.
-    Private.finalizeTabPosition(data, this._orientation);
+    Private.finalizeTabPosition(data, this._orientation, this._scrollState);
 
     // Remove the dragging class from the tab so it can be transitioned.
     data.tab.classList.remove('lm-mod-dragging');
@@ -1207,7 +1421,9 @@ export class TabBar<T> extends Widget {
   private _titlesEditable: boolean = false;
   private _previousTitle: Title<T> | null = null;
   private _dragData: Private.IDragData | null = null;
+  private _scrollData: Private.IScrollData | null = null;
   private _addButtonEnabled: boolean = false;
+  private _scrollingEnabled: boolean = false;
   private _tabMoved = new Signal<this, TabBar.ITabMovedArgs<T>>(this);
   private _currentChanged = new Signal<this, TabBar.ICurrentChangedArgs<T>>(
     this
@@ -1709,6 +1925,33 @@ namespace Private {
    */
   export const DETACH_THRESHOLD = 20;
 
+  /**
+   * A struct which holds the scroll data for a tab bar.
+   */
+  export interface IScrollData {
+    timerHandle: number;
+    scrollDirection: '+' | '-';
+    rate: number;
+  }
+
+  /**
+   * A struct which holds the scroll state for a tab bar.
+   */
+  export interface IScrollState {
+    /**
+     * The size of the container where scrolling occurs (the visible part of the total).
+     */
+    displayedSize: number;
+    /**
+     * The total size of the content to be scrolled.
+     */
+    totalSize: number;
+    /**
+     * The current position (offset) of the scroll state.
+     */
+    position: number;
+  }
+
   /**
    * A struct which holds the drag data for a tab bar.
    */
@@ -1728,6 +1971,11 @@ namespace Private {
      */
     pressX: number;
 
+    /**
+     * The initial scroll position
+     */
+    initialScrollPosition: number;
+
     /**
      * The mouse press client Y position.
      */
@@ -1823,13 +2071,32 @@ namespace Private {
    */
   export function createNode(): HTMLDivElement {
     let node = document.createElement('div');
+    let scrollBefore = document.createElement('div');
+    scrollBefore.className =
+      'lm-TabBar-button lm-TabBar-scrollButton lm-TabBar-scrollBeforeButton lm-mod-hidden';
+    scrollBefore.setAttribute('role', 'button');
+    scrollBefore.innerText = '<';
+    let scrollAfter = document.createElement('div');
+    scrollAfter.className =
+      'lm-TabBar-button lm-TabBar-scrollButton lm-TabBar-scrollAfterButton lm-mod-hidden';
+    scrollAfter.setAttribute('role', 'button');
+    scrollAfter.innerText = '>';
+    node.appendChild(scrollBefore);
+
     let content = document.createElement('ul');
     content.setAttribute('role', 'tablist');
     content.className = 'lm-TabBar-content';
-    node.appendChild(content);
+
+    let wrapper = document.createElement('div');
+    wrapper.className = 'lm-TabBar-wrapper';
+    wrapper.appendChild(content);
+    node.appendChild(wrapper);
+
+    node.appendChild(scrollAfter);
 
     let add = document.createElement('div');
-    add.className = 'lm-TabBar-addButton lm-mod-hidden';
+    add.setAttribute('role', 'button');
+    add.className = 'lm-TabBar-button lm-TabBar-addButton lm-mod-hidden';
     node.appendChild(add);
     return node;
   }
@@ -1906,23 +2173,24 @@ namespace Private {
     tabs: HTMLCollection,
     data: IDragData,
     event: MouseEvent,
-    orientation: TabBar.Orientation
+    orientation: TabBar.Orientation,
+    scrollState: IScrollState
   ): void {
     // Compute the orientation-sensitive values.
     let pressPos: number;
     let localPos: number;
     let clientPos: number;
-    let clientSize: number;
+    const clientSize = scrollState.totalSize;
+    const scrollShift = scrollState.position - data.initialScrollPosition;
+
     if (orientation === 'horizontal') {
-      pressPos = data.pressX;
-      localPos = event.clientX - data.contentRect!.left;
+      pressPos = data.pressX - scrollShift;
+      localPos = event.clientX - data.contentRect!.left + scrollState.position;
       clientPos = event.clientX;
-      clientSize = data.contentRect!.width;
     } else {
-      pressPos = data.pressY;
-      localPos = event.clientY - data.contentRect!.top;
+      pressPos = data.pressY - scrollShift;
+      localPos = event.clientY - data.contentRect!.top + scrollState.position;
       clientPos = event.clientY;
-      clientSize = data.contentRect!.height;
     }
 
     // Compute the target data.
@@ -1964,15 +2232,10 @@ namespace Private {
    */
   export function finalizeTabPosition(
     data: IDragData,
-    orientation: TabBar.Orientation
+    orientation: TabBar.Orientation,
+    scrollState: IScrollState
   ): void {
-    // Compute the orientation-sensitive client size.
-    let clientSize: number;
-    if (orientation === 'horizontal') {
-      clientSize = data.contentRect!.width;
-    } else {
-      clientSize = data.contentRect!.height;
-    }
+    const clientSize = scrollState.totalSize;
 
     // Compute the ideal final tab position.
     let ideal: number;
diff --git a/packages/widgets/style/tabbar.css b/packages/widgets/style/tabbar.css
index fabd830e7..9b0ce9cd9 100644
--- a/packages/widgets/style/tabbar.css
+++ b/packages/widgets/style/tabbar.css
@@ -33,11 +33,11 @@
   list-style-type: none;
 }
 
-.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-content {
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper > .lm-TabBar-content {
   flex-direction: row;
 }
 
-.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-content {
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper > .lm-TabBar-content {
   flex-direction: column;
 }
 
@@ -98,3 +98,127 @@
   box-sizing: border-box;
   background: inherit;
 }
+
+.lm-TabBar-wrapper {
+  /* Remove wrapper from DOM when scrolling is not enabled */
+  display: contents;
+  --lm-tabbar-scrollshadow-end: rgba(0, 0, 0, 0.3);
+  position: relative;
+}
+
+.lm-TabBar.lm-mod-scrollable .lm-TabBar-wrapper {
+  /* Keep wrapper in DOM when scrolling is enabled */
+  display: block;
+}
+
+.lm-TabBar-content {
+  scrollbar-width: none;
+}
+
+.lm-TabBar-content::-webkit-scrollbar {
+  display: none;
+}
+
+.lm-TabBar[data-orientation='horizontal'].lm-mod-scrollable
+  > .lm-TabBar-wrapper {
+  overflow-x: hidden;
+  overflow-y: hidden;
+}
+
+.lm-TabBar[data-orientation='vertical'].lm-mod-scrollable > .lm-TabBar-wrapper {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.lm-TabBar > .lm-TabBar-wrapper > .lm-TabBar-content {
+  width: 100%;
+}
+
+.lm-TabBar[data-orientation='horizontal'].lm-mod-scrollable
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
+  overflow-x: scroll;
+  overflow-y: hidden;
+}
+
+.lm-TabBar[data-orientation='vertical'].lm-mod-scrollable
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
+  overflow-x: hidden;
+  overflow-y: scroll;
+}
+
+.lm-TabBar-wrapper::before,
+.lm-TabBar-wrapper::after {
+  content: '';
+  z-index: 10000;
+  display: none;
+  position: absolute;
+}
+
+.lm-TabBar-wrapper[data-can-scroll='before']::before {
+  display: block;
+}
+
+.lm-TabBar-wrapper[data-can-scroll='after']::after {
+  display: block;
+}
+
+.lm-TabBar-wrapper[data-can-scroll='both']::before,
+.lm-TabBar-wrapper[data-can-scroll='both']::after {
+  display: block;
+}
+
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::before,
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::after {
+  width: 5px;
+  height: 100%;
+  top: 0;
+}
+
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::before {
+  background: linear-gradient(
+    to left,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  left: 0;
+}
+
+.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper::after {
+  background: linear-gradient(
+    to right,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  right: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::before,
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::after {
+  height: 5px;
+  width: 100%;
+  left: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::before {
+  background: linear-gradient(
+    to top,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  top: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper::after {
+  background: linear-gradient(
+    to bottom,
+    rgba(0, 0, 0, 0),
+    var(--lm-tabbar-scrollshadow-end)
+  );
+  bottom: 0;
+}
+
+.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-scrollButton {
+  transform: rotate(90deg);
+}

From de12bf88f1ba20c98e6e75b18e16b09b69d39e12 Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Mon, 28 Mar 2022 17:50:50 +0100
Subject: [PATCH 2/8] Implement scrolling to active tab, fix styles, add
 release guard

---
 packages/widgets/src/tabbar.ts    | 34 ++++++++++++++++++++++++++++++-
 packages/widgets/style/tabbar.css |  2 +-
 2 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index bb66dcbf4..86050efa9 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -367,7 +367,7 @@ export class TabBar<T> extends Widget {
     if (value) {
       this.node.classList.add('lm-mod-scrollable');
     } else {
-      this.node.classList.add('lm-mod-scrollable');
+      this.node.classList.remove('lm-mod-scrollable');
     }
     this.maybeSwitchScrollButtons();
   }
@@ -694,7 +694,11 @@ export class TabBar<T> extends Widget {
       content[i] = renderer.renderTab({ title, current, zIndex });
     }
     VirtualDOM.render(content, this.contentNode);
+
     this.maybeSwitchScrollButtons();
+
+    // Scroll the current tab into view.
+    this.scrollCurrentIntoView();
   }
 
   protected onResize(msg: Widget.ResizeMessage): void {
@@ -702,6 +706,29 @@ export class TabBar<T> extends Widget {
     this.maybeSwitchScrollButtons();
   }
 
+  /**
+   * Scroll the current tab into view.
+   */
+  protected scrollCurrentIntoView() {
+    if (this.scrollingEnabled) {
+      const contentNode = this.contentNode;
+      const currentNode = contentNode.children.item(this.currentIndex);
+      if (currentNode) {
+        currentNode.scrollIntoView();
+        if (this.orientation == 'horizontal') {
+          contentNode.scrollTop = 0;
+        } else {
+          contentNode.scrollLeft = 0;
+        }
+      } else {
+        console.error('Current tab node not found');
+      }
+    }
+  }
+
+  /**
+   * Show/hide scroll buttons if needed.
+   */
   protected maybeSwitchScrollButtons() {
     const scrollBefore = this.scrollBeforeButtonNode;
     const scrollAfter = this.scrollAfterButtonNode;
@@ -1152,6 +1179,11 @@ export class TabBar<T> extends Widget {
         return;
       }
 
+      // Do nothing if neither press nor release was on a tab.
+      if (index === -1 && data.index === -1) {
+        return;
+      }
+
       // Ignore the release if the title is not closable.
       let title = this._titles[index];
       if (!title.closable) {
diff --git a/packages/widgets/style/tabbar.css b/packages/widgets/style/tabbar.css
index 9b0ce9cd9..d9b79e41a 100644
--- a/packages/widgets/style/tabbar.css
+++ b/packages/widgets/style/tabbar.css
@@ -70,7 +70,7 @@
   display: none !important;
 }
 
-.lm-TabBar-addButton.lm-mod-hidden {
+.lm-TabBar-button.lm-mod-hidden {
   display: none !important;
 }
 

From 2db072434b80475d28a470af2ac0a21c3ac045b0 Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Mon, 28 Mar 2022 23:10:17 +0100
Subject: [PATCH 3/8] Improve scrolling: smoother, better draging, wheel
 support

- smoother: use `requestAnimationFrame` instead of `setInterval`
- dragging:
  - detach by scrolling (requires scroll delta computation)
  - update drag when scrolling (by dispatching last event again to
    invoke handler as if mouse moved)
- wheel: allow to scroll using mouse wheel
---
 packages/widgets/src/tabbar.ts | 161 ++++++++++++++++++++++++---------
 1 file changed, 117 insertions(+), 44 deletions(-)

diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index 86050efa9..2335b762a 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -636,15 +636,19 @@ export class TabBar<T> extends Widget {
   handleEvent(event: Event): void {
     switch (event.type) {
       case 'pointerdown':
-        this._evtPointerDown(event as PointerEvent);
+        this._lastMouseEvent = event as MouseEvent;
+        this._evtPointerDown(event as MouseEvent);
         break;
       case 'pointermove':
-        this._evtPointerMove(event as PointerEvent);
+        this._lastMouseEvent = event as MouseEvent;
+        this._evtPointerMove(event as MouseEvent);
         break;
       case 'pointerup':
-        this._evtPointerUp(event as PointerEvent);
+        this._lastMouseEvent = event as MouseEvent;
+        this._evtPointerUp(event as MouseEvent);
         break;
       case 'dblclick':
+        this._lastMouseEvent = event as MouseEvent;
         this._evtDblClick(event as MouseEvent);
         break;
       case 'keydown':
@@ -657,6 +661,10 @@ export class TabBar<T> extends Widget {
       case 'scroll':
         this._evtScroll(event);
         break;
+      case 'wheel':
+        this._evtWheel(event as WheelEvent);
+        event.preventDefault();
+        break;
     }
   }
 
@@ -667,6 +675,7 @@ export class TabBar<T> extends Widget {
     this.node.addEventListener('pointerdown', this);
     this.node.addEventListener('dblclick', this);
     this.contentNode.addEventListener('scroll', this);
+    this.contentNode.addEventListener('wheel', this);
   }
 
   /**
@@ -676,6 +685,7 @@ export class TabBar<T> extends Widget {
     this.node.removeEventListener('pointerdown', this);
     this.node.removeEventListener('dblclick', this);
     this.contentNode.removeEventListener('scroll', this);
+    this.contentNode.removeEventListener('wheel', this);
     this._releaseMouse();
   }
 
@@ -797,6 +807,10 @@ export class TabBar<T> extends Widget {
     this.updateScrollingHints(this._scrollState);
   }
 
+  private _evtWheel(event: WheelEvent): void {
+    this.scrollBy(event.deltaY);
+  }
+
   /**
    * Handle the `'dblclick'` event for the tab bar.
    */
@@ -874,25 +888,47 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  protected scrollBy(change: number) {
+    const orientation = this.orientation;
+    const contentNode = this.contentNode;
+
+    if (orientation == 'horizontal') {
+      contentNode.scrollLeft += change;
+    } else {
+      contentNode.scrollTop += change;
+    }
+    // Force-update drag state by dispatching last recorded mouse event.
+    if (this._lastMouseEvent) {
+      this._evtPointerMove(this._lastMouseEvent);
+    }
+  }
+
   protected beginScrolling(direction: '-' | '+') {
-    const initialRate = 5;
-    const rateIncrease = 1;
-    const maxRate = 20;
-    const intervalHandle = setInterval(() => {
+    // How many pixels should be scrolled per second initially?
+    const initialRate = 150;
+    // By how much should the scrolling rate increase per second?
+    const rateIncrease = 80;
+    // What should be the maximal scrolling speed (pixels/second?)
+    const maxRate = 450;
+
+    let previousTime = performance.now();
+
+    const step = () => {
       if (!this._scrollData) {
         this.stopScrolling();
         return;
       }
+      const stepTime = performance.now();
+      const secondsChange = (stepTime - previousTime) / 1000;
+      previousTime = stepTime;
       const rate = this._scrollData.rate;
-      const direction = this._scrollData.scrollDirection;
-      const change = (direction == '+' ? 1 : -1) * rate;
-      if (this.orientation == 'horizontal') {
-        this.contentNode.scrollLeft += change;
-      } else {
-        this.contentNode.scrollTop += change;
-      }
+      const direction = this._scrollData.direction;
+
+      const change = (direction == '+' ? 1 : -1) * rate * secondsChange;
+
+      this.scrollBy(change);
       this._scrollData.rate = Math.min(
-        this._scrollData.rate + rateIncrease,
+        this._scrollData.rate + rateIncrease * secondsChange,
         maxRate
       );
       const state = this._scrollState;
@@ -902,18 +938,26 @@ export class TabBar<T> extends Widget {
           state.totalSize == state.position + state.displayedSize)
       ) {
         this.stopScrolling();
+        return;
       }
-    }, 50);
+      window.requestAnimationFrame(step);
+    };
+
+    const shouldRequest = !this._scrollData;
+
     this._scrollData = {
-      timerHandle: intervalHandle,
-      scrollDirection: direction,
+      direction: direction,
       rate: initialRate
     };
+
+    if (shouldRequest) {
+      window.requestAnimationFrame(step);
+    }
   }
 
   protected stopScrolling() {
-    if (this._scrollData) {
-      clearInterval(this._scrollData.timerHandle);
+    if (!this._scrollData) {
+      return;
     }
     this._scrollData = null;
     const state = this._scrollState;
@@ -969,6 +1013,18 @@ export class TabBar<T> extends Widget {
     event.preventDefault();
     event.stopPropagation();
 
+    // Add the document mouse up listener.
+    this.document.addEventListener('pointerup', this, true);
+
+    // Do nothing else if the middle button or add button is clicked.
+    if (event.button === 1 || addButtonClicked) {
+      return;
+    }
+    if (scrollBeforeButtonClicked || scrollAfterButtonClicked) {
+      this.beginScrolling(scrollBeforeButtonClicked ? '-' : '+');
+      return;
+    }
+
     // Initialize the non-measured parts of the drag data.
     this._dragData = {
       tab: tabs[index] as HTMLElement,
@@ -1039,6 +1095,21 @@ export class TabBar<T> extends Widget {
    * Handle the `'pointermove'` event for the tab bar.
    */
   private _evtPointerMove(event: PointerEvent | MouseEvent): void {
+    let overBeforeScrollButton =
+      this.scrollingEnabled &&
+      this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
+
+    let overAfterScrollButton =
+      this.scrollingEnabled &&
+      this.scrollAfterButtonNode.contains(event.target as HTMLElement);
+
+    const isOverScrollButton = overBeforeScrollButton || overAfterScrollButton;
+
+    if (!isOverScrollButton) {
+      // Stop scrolling if mouse is not over scroll buttons
+      this.stopScrolling();
+    }
+
     // Do nothing if no drag is in progress.
     let data = this._dragData;
     if (!data) {
@@ -1049,14 +1120,22 @@ export class TabBar<T> extends Widget {
     event.preventDefault();
     event.stopPropagation();
 
-    // Lookup the tab nodes.
-    let tabs = this.contentNode.children;
+    if (isOverScrollButton) {
+      // Start scrolling if the mouse is over scroll buttons
+      this.beginScrolling(overBeforeScrollButton ? '-' : '+');
+    }
 
     // Bail early if the drag threshold has not been met.
-    if (!data.dragActive && !Private.dragExceeded(data, event)) {
+    if (
+      !data.dragActive &&
+      !Private.dragExceeded(data, event, this._scrollState)
+    ) {
       return;
     }
 
+    // Lookup the tab nodes.
+    let tabs = this.contentNode.children;
+
     // Activate the drag if necessary.
     if (!data.dragActive) {
       // Fill in the rest of the drag data measurements.
@@ -1103,22 +1182,6 @@ export class TabBar<T> extends Widget {
       }
     }
 
-    let overBeforeScrollButton =
-      this.scrollingEnabled &&
-      this.scrollBeforeButtonNode.contains(event.target as HTMLElement);
-
-    let overAfterScrollButton =
-      this.scrollingEnabled &&
-      this.scrollAfterButtonNode.contains(event.target as HTMLElement);
-
-    if (overBeforeScrollButton || overAfterScrollButton) {
-      // Start scrolling if the mouse is over scroll buttons
-      this.beginScrolling(overBeforeScrollButton ? '-' : '+');
-    } else {
-      // Stop scrolling if mouse is not over scroll buttons
-      this.stopScrolling();
-    }
-
     // Update the positions of the tabs.
     Private.layoutTabs(tabs, data, event, this._orientation, this._scrollState);
   }
@@ -1135,6 +1198,9 @@ export class TabBar<T> extends Widget {
     // Do nothing if no drag is in progress.
     const data = this._dragData;
     if (!data) {
+      if (this._scrollData) {
+        this.stopScrolling();
+      }
       return;
     }
 
@@ -1450,6 +1516,7 @@ export class TabBar<T> extends Widget {
   private _titles: Title<T>[] = [];
   private _orientation: TabBar.Orientation;
   private _document: Document | ShadowRoot;
+  private _lastMouseEvent: MouseEvent | null = null;
   private _titlesEditable: boolean = false;
   private _previousTitle: Title<T> | null = null;
   private _dragData: Private.IDragData | null = null;
@@ -1961,8 +2028,7 @@ namespace Private {
    * A struct which holds the scroll data for a tab bar.
    */
   export interface IScrollData {
-    timerHandle: number;
-    scrollDirection: '+' | '-';
+    direction: '+' | '-';
     rate: number;
   }
 
@@ -2177,12 +2243,19 @@ namespace Private {
   }
 
   /**
-   * Test if the event exceeds the drag threshold.
+   * Test if the event or scroll state exceeds the drag threshold.
    */
-  export function dragExceeded(data: IDragData, event: MouseEvent): boolean {
+  export function dragExceeded(
+    data: IDragData,
+    event: MouseEvent,
+    scrollState: IScrollState | null
+  ): boolean {
     let dx = Math.abs(event.clientX - data.pressX);
     let dy = Math.abs(event.clientY - data.pressY);
-    return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD;
+    let ds = scrollState
+      ? Math.abs(data.initialScrollPosition - scrollState.position)
+      : 0;
+    return dx >= DRAG_THRESHOLD || dy >= DRAG_THRESHOLD || ds >= DRAG_THRESHOLD;
   }
 
   /**

From c6507cb75841a3ceefe9ddeb4412e92b14e0c59b Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Mon, 28 Mar 2022 23:27:35 +0100
Subject: [PATCH 4/8] Add docstrings

---
 packages/widgets/src/tabbar.ts | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index 2335b762a..742d9a064 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -801,12 +801,15 @@ export class TabBar<T> extends Widget {
   }
 
   /**
-   * Handle the `'dblclick'` event for the tab bar.
+   * Handle the `'scroll'` event for the tab bar.
    */
   private _evtScroll(event: Event): void {
     this.updateScrollingHints(this._scrollState);
   }
 
+  /**
+   * Handle the `'wheel'` event for the tab bar.
+   */
   private _evtWheel(event: WheelEvent): void {
     this.scrollBy(event.deltaY);
   }
@@ -888,6 +891,9 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  /**
+   * Scroll by a fixed number of pixels (sign defines direction).
+   */
   protected scrollBy(change: number) {
     const orientation = this.orientation;
     const contentNode = this.contentNode;
@@ -903,6 +909,9 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  /**
+   * Begin scrolling in given direction.
+   */
   protected beginScrolling(direction: '-' | '+') {
     // How many pixels should be scrolled per second initially?
     const initialRate = 150;
@@ -955,6 +964,9 @@ export class TabBar<T> extends Widget {
     }
   }
 
+  /**
+   * Stop scrolling which was started with `beginScrolling` method.
+   */
   protected stopScrolling() {
     if (!this._scrollData) {
       return;

From 82ea6f67cecbda7f9ff874e02be9f4dceaa08a0b Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Tue, 29 Mar 2022 00:07:44 +0100
Subject: [PATCH 5/8] Decouple drag target index from click target index

Reducing the complexity of the logic and fixing tab add/close actions
---
 packages/widgets/src/tabbar.ts | 42 ++++++++++++++++++++--------------
 1 file changed, 25 insertions(+), 17 deletions(-)

diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index 742d9a064..c1296d332 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -1028,16 +1028,25 @@ export class TabBar<T> extends Widget {
     // Add the document mouse up listener.
     this.document.addEventListener('pointerup', this, true);
 
+    if (scrollBeforeButtonClicked || scrollAfterButtonClicked) {
+      this.beginScrolling(scrollBeforeButtonClicked ? '-' : '+');
+      return;
+    }
+
+    this._clickedTabIndex = index;
+
     // Do nothing else if the middle button or add button is clicked.
     if (event.button === 1 || addButtonClicked) {
       return;
     }
-    if (scrollBeforeButtonClicked || scrollAfterButtonClicked) {
-      this.beginScrolling(scrollBeforeButtonClicked ? '-' : '+');
+
+    // Do nothing else if the close icon is clicked.
+    let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
+    if (icon && icon.contains(event.target as HTMLElement)) {
       return;
     }
 
-    // Initialize the non-measured parts of the drag data.
+    // Initialize the non-measured parts of the drag data,
     this._dragData = {
       tab: tabs[index] as HTMLElement,
       index: index,
@@ -1207,14 +1216,7 @@ export class TabBar<T> extends Widget {
       return;
     }
 
-    // Do nothing if no drag is in progress.
     const data = this._dragData;
-    if (!data) {
-      if (this._scrollData) {
-        this.stopScrolling();
-      }
-      return;
-    }
 
     // Stop the event propagation.
     event.preventDefault();
@@ -1223,17 +1225,22 @@ export class TabBar<T> extends Widget {
     // Remove the extra mouse event listeners.
     this.document.removeEventListener('pointermove', this, true);
     this.document.removeEventListener('pointerup', this, true);
-    this.document.removeEventListener('keydown', this, true);
-    this.document.removeEventListener('contextmenu', this, true);
 
-    // Handle a release when the drag is not active.
-    if (!data.dragActive) {
+    // Remove extra mouse event listeners which are only added when drag is in progress.
+    if (data) {
+      this.document.removeEventListener('pointermove', this, true);
+      this.document.removeEventListener('keydown', this, true);
+      this.document.removeEventListener('contextmenu', this, true);
+    }
+
+    // Handle a release when the drag is not active or not in progress.
+    if (!data || !data.dragActive) {
       // Clear the drag data.
       this._dragData = null;
 
+      // Handle mouse release if scrolling was in progress.
       if (this._scrollData) {
         this.stopScrolling();
-        return;
       }
       // Handle clicking the add button.
       let addButtonClicked =
@@ -1253,12 +1260,12 @@ export class TabBar<T> extends Widget {
       });
 
       // Do nothing if the release is not on the original pressed tab.
-      if (index !== data.index) {
+      if (index !== this._clickedTabIndex) {
         return;
       }
 
       // Do nothing if neither press nor release was on a tab.
-      if (index === -1 && data.index === -1) {
+      if (index === -1 && this._clickedTabIndex === -1) {
         return;
       }
 
@@ -1524,6 +1531,7 @@ export class TabBar<T> extends Widget {
   }
 
   private _name: string;
+  private _clickedTabIndex: number = -1;
   private _currentIndex = -1;
   private _titles: Title<T>[] = [];
   private _orientation: TabBar.Orientation;

From 59bf5f1b4f911d5f2de1f3c7163f1433cbcdc84f Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Thu, 28 Jul 2022 18:49:40 +0100
Subject: [PATCH 6/8] Fix typos in docstrings

---
 packages/widgets/src/tabbar.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index c1296d332..1c1aedd49 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -352,6 +352,8 @@ export class TabBar<T> extends Widget {
 
   /**
    * Whether scrolling is enabled.
+   *
+   * Note: for scrolling to work the tabs need to have `min-width` set.
    */
   get scrollingEnabled(): boolean {
     return this._scrollingEnabled;
@@ -397,7 +399,7 @@ export class TabBar<T> extends Widget {
    * The tab bar content wrapper node.
    *
    * #### Notes
-   * This is the node which the content node and enables scrolling.
+   * This is the node which wraps the content node and enables scrolling.
    *
    * Modifying this node directly can lead to undefined behavior.
    */
@@ -1046,7 +1048,7 @@ export class TabBar<T> extends Widget {
       return;
     }
 
-    // Initialize the non-measured parts of the drag data,
+    // Initialize the non-measured parts of the drag data.
     this._dragData = {
       tab: tabs[index] as HTMLElement,
       index: index,

From 9345a612a8f12c7278bf33967edd3918d639df3a Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Thu, 28 Jul 2022 18:49:59 +0100
Subject: [PATCH 7/8] Add initial tests

---
 packages/widgets/tests/src/tabbar.spec.ts | 33 +++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/packages/widgets/tests/src/tabbar.spec.ts b/packages/widgets/tests/src/tabbar.spec.ts
index 082eb979c..3ec977012 100644
--- a/packages/widgets/tests/src/tabbar.spec.ts
+++ b/packages/widgets/tests/src/tabbar.spec.ts
@@ -144,6 +144,7 @@ describe('@lumino/widgets', () => {
           tabsMovable: true,
           allowDeselect: true,
           addButtonEnabled: true,
+          scrollingEnabled: true,
           insertBehavior: 'select-tab',
           removeBehavior: 'select-previous-tab',
           renderer
@@ -152,6 +153,7 @@ describe('@lumino/widgets', () => {
         expect(newBar.tabsMovable).to.equal(true);
         expect(newBar.renderer).to.equal(renderer);
         expect(newBar.addButtonEnabled).to.equal(true);
+        expect(newBar.scrollingEnabled).to.equal(true);
       });
 
       it('should add the `lm-TabBar` class', () => {
@@ -652,6 +654,37 @@ describe('@lumino/widgets', () => {
       });
     });
 
+    describe('#scrollingEnabled', () => {
+      it('should get whether the scroll buttons are enabled', () => {
+        let bar = new TabBar<Widget>();
+        expect(bar.scrollingEnabled).to.equal(false);
+      });
+
+      it('should set whether the scroll buttons are enabled', () => {
+        let bar = new TabBar<Widget>();
+        bar.scrollingEnabled = true;
+        expect(bar.scrollingEnabled).to.equal(true);
+      });
+
+      it('should not show the scroll buttons if not set', () => {
+        populateBar(bar);
+        expect(
+          bar.scrollBeforeButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(true);
+        expect(
+          bar.scrollAfterButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(true);
+
+        bar.scrollingEnabled = true;
+        expect(
+          bar.scrollBeforeButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(false);
+        expect(
+          bar.scrollAfterButtonNode.classList.contains('lm-mod-hidden')
+        ).to.equal(true);
+      });
+    });
+
     describe('#allowDeselect', () => {
       it('should determine whether a tab can be deselected by the user', () => {
         populateBar(bar);

From c4e15130f0cda4a059576ee35fd054bb1c2562c8 Mon Sep 17 00:00:00 2001
From: krassowski <5832902+krassowski@users.noreply.github.com>
Date: Wed, 3 Aug 2022 17:43:47 +0100
Subject: [PATCH 8/8] Fix rebase issues, lint, add missing option

---
 packages/widgets/src/tabbar.ts    | 26 ++++++++------------------
 packages/widgets/style/tabbar.css |  8 ++++++--
 2 files changed, 14 insertions(+), 20 deletions(-)

diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts
index 1c1aedd49..39d506ecb 100644
--- a/packages/widgets/src/tabbar.ts
+++ b/packages/widgets/src/tabbar.ts
@@ -54,6 +54,7 @@ export class TabBar<T> extends Widget {
     this._document = options.document || document;
     this.tabsMovable = options.tabsMovable || false;
     this.titlesEditable = options.titlesEditable || false;
+    this._scrollingEnabled = options.scrollingEnabled || false;
     this.allowDeselect = options.allowDeselect || false;
     this.addButtonEnabled = options.addButtonEnabled || false;
     this.insertBehavior = options.insertBehavior || 'select-tab-if-needed';
@@ -1070,24 +1071,6 @@ export class TabBar<T> extends Widget {
       detachRequested: false
     };
 
-    // Add the document pointer up listener.
-    this.document.addEventListener('pointerup', this, true);
-
-    // Do nothing else if the middle button or add button is clicked.
-    if (event.button === 1 || addButtonClicked) {
-      return;
-    }
-    if (scrollBeforeButtonClicked || scrollAfterButtonClicked) {
-      this.beginScrolling(scrollBeforeButtonClicked ? '-' : '+');
-      return;
-    }
-
-    // Do nothing else if the close icon is clicked.
-    let icon = tabs[index].querySelector(this.renderer.closeIconSelector);
-    if (icon && icon.contains(event.target as HTMLElement)) {
-      return;
-    }
-
     // Add the extra listeners if the tabs are movable.
     if (this.tabsMovable) {
       this.document.addEventListener('pointermove', this, true);
@@ -1683,6 +1666,13 @@ export namespace TabBar {
      */
     addButtonEnabled?: boolean;
 
+    /**
+     * Whether scrolling is enabled.
+     *
+     * The default is `false`.
+     */
+    scrollingEnabled?: boolean;
+
     /**
      * The selection behavior when inserting a tab.
      *
diff --git a/packages/widgets/style/tabbar.css b/packages/widgets/style/tabbar.css
index d9b79e41a..5ad59d53b 100644
--- a/packages/widgets/style/tabbar.css
+++ b/packages/widgets/style/tabbar.css
@@ -33,11 +33,15 @@
   list-style-type: none;
 }
 
-.lm-TabBar[data-orientation='horizontal'] > .lm-TabBar-wrapper > .lm-TabBar-content {
+.lm-TabBar[data-orientation='horizontal']
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
   flex-direction: row;
 }
 
-.lm-TabBar[data-orientation='vertical'] > .lm-TabBar-wrapper > .lm-TabBar-content {
+.lm-TabBar[data-orientation='vertical']
+  > .lm-TabBar-wrapper
+  > .lm-TabBar-content {
   flex-direction: column;
 }