diff --git a/chrome/content/zotero/components/tabBar.jsx b/chrome/content/zotero/components/tabBar.jsx
index 98b1897ddb..c9fcd34676 100644
--- a/chrome/content/zotero/components/tabBar.jsx
+++ b/chrome/content/zotero/components/tabBar.jsx
@@ -33,7 +33,7 @@ const { CSSIcon, CSSItemTypeIcon } = require('./icons');
const SCROLL_ARROW_SCROLL_BY = 222;
const Tab = memo((props) => {
- const { icon, id, index, isBeingDragged, isItemType, onContextMenu, onDragEnd, onDragStart, onTabClick, onTabClose, onTabMouseDown, selected, title, renderTitle } = props;
+ const { icon, id, index, isBeingDragged, isItemType, onContextMenu, onDragEnd, onDragStart, onTabClick, onTabClose, onTabMouseDown, selected, title, renderTitle, width } = props;
const handleTabMouseDown = useCallback(event => onTabMouseDown(event, id), [onTabMouseDown, id]);
const handleContextMenu = useCallback(event => onContextMenu(event, id), [onContextMenu, id]);
@@ -66,6 +66,7 @@ const Tab = memo((props) => {
onDragStart={handleDragStart}
onDragEnd={onDragEnd}
tabIndex="-1"
+ style={ width ? { maxWidth: `${width}px` } : {}}
>
{ isItemType
?
@@ -100,6 +101,7 @@ Tab.propTypes = {
selected: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
renderTitle: PropTypes.bool,
+ width: PropTypes.number
};
@@ -107,10 +109,12 @@ const TabBar = forwardRef(function (props, ref) {
const [tabs, setTabs] = useState([]);
const [dragging, setDragging] = useState(false);
const [dragMouseX, setDragMouseX] = useState(0);
+ const [lockedTabsState, setLockedTabsState] = useState(new Map());
const dragIDRef = useRef(null);
const dragGrabbedDeltaXRef = useRef();
const tabsInnerContainerRef = useRef();
const tabsRef = useRef();
+ const spacerRef = useRef();
const startArrowRef = useRef();
const endArrowRef = useRef();
// Used to throttle mouse movement
@@ -133,10 +137,11 @@ const TabBar = forwardRef(function (props, ref) {
useEffect(() => {
// Scroll selected tab into view
let selectedTabNode = tabsInnerContainerRef.current.querySelector(".tab.selected");
- if (!selectedTabNode || dragging) return;
+ if (!selectedTabNode || dragging || lockedTabsState.size) return;
selectedTabNode.scrollIntoView({ behavior: 'smooth' });
}, [tabs]);
+ useLayoutEffect(preserveScrollPosition, [tabs, lockedTabsState]);
useLayoutEffect(updateScrollArrows);
useLayoutEffect(updateOverflowing, [tabs]);
@@ -170,6 +175,7 @@ const TabBar = forwardRef(function (props, ref) {
});
function updateScrollArrows() {
+ if (lockedTabsState.size) return;
let scrollable = tabsRef.current.scrollWidth !== tabsRef.current.clientWidth;
if (scrollable) {
tabsInnerContainerRef.current.classList.add('scrollable');
@@ -198,6 +204,23 @@ const TabBar = forwardRef(function (props, ref) {
tabNameDOM.classList.toggle('overflowing', tabNameDOM.scrollWidth > tabNameDOM.clientWidth);
});
}
+
+ // Preserve the relative scroll position when tabs are being closed.
+ // (set in handleTabClose)
+ function preserveScrollPosition() {
+ let desiredScrollLeft = lockedTabsState.get("scroll-left");
+ if (!desiredScrollLeft) return;
+ tabsRef.current.scrollLeft = desiredScrollLeft;
+ // If there are not many tabs left, there may be not enough scrollable
+ // space to keep the original scrollLeft position. Add a spacer at the end and try again.
+ if (tabsRef.current.scrollLeft !== desiredScrollLeft) {
+ let spacerWidth = parseInt(spacerRef.current.style.minWidth) || 0;
+ let columnGap = 4;
+ let tabWidth = lockedTabsState.get("tab") || 0;
+ spacerRef.current.style.minWidth = `${spacerWidth + tabWidth + columnGap}px`;
+ tabsRef.current.scrollLeft = desiredScrollLeft;
+ }
+ }
const handleTabMouseDown = useCallback((event, id) => {
// Don't select tab if it'll be closed with middle button click on mouse up
@@ -313,10 +336,29 @@ const TabBar = forwardRef(function (props, ref) {
}, [props.onTabMove]);
const handleTabClose = useCallback((event, id) => {
+ // When a tab is being closed, lock the widths of tabs and scroll position,
+ // so that the next tab's "X" button also ends up under the mouse
+ if (!lockedTabsState.size) {
+ let tabElements = [...tabsInnerContainerRef.current.querySelectorAll(".tab")].filter(tab => tab.offsetWidth);
+ let zoteroPaneTab = tabElements.find(tab => tab.dataset.id === "zotero-pane");
+ let tab = tabElements.find(tab => tab.dataset.id !== "zotero-pane");
+ setLockedTabsState(new Map([
+ ["zotero-pane", zoteroPaneTab.offsetWidth],
+ ["tab", tab.offsetWidth],
+ ["scroll-left", tabsRef.current.scrollLeft]
+ ]));
+ }
props.onTabClose(id);
event.stopPropagation();
- }, [props.onTabClose]);
-
+ }, [props.onTabClose, lockedTabsState]);
+
+ // When the mouse leaves the tab bar, un-freeze the width and positioning
+ // of tabs that are locked when tabs are being closed.
+ const handleTabBarMouseLeave = useCallback(() => {
+ if (!lockedTabsState.size) return;
+ setLockedTabsState(new Map());
+ spacerRef.current.style.removeProperty("min-width");
+ }, [lockedTabsState]);
const handleWheel = useCallback((event) => {
// Normalize wheel speed
@@ -359,6 +401,7 @@ const TabBar = forwardRef(function (props, ref) {
ref={tabsInnerContainerRef}
className="tab-bar-inner-container"
onWheel={handleWheel}
+ onMouseLeave={handleTabBarMouseLeave}
>