From 5d442b706e757847549015b99bdb0c684602ea71 Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Wed, 20 Apr 2022 05:51:32 +0700 Subject: [PATCH] Make tab bar scrollable (#2506) Fixes #2232 --- chrome/content/zotero/components/icons.jsx | 16 +++ chrome/content/zotero/components/tabBar.jsx | 118 ++++++++++++++++++-- chrome/content/zotero/tabs.js | 13 +++ scss/components/_tabBar.scss | 59 ++++++++++ scss/mac/_tabBar.scss | 12 +- 5 files changed, 199 insertions(+), 19 deletions(-) diff --git a/chrome/content/zotero/components/icons.jsx b/chrome/content/zotero/components/icons.jsx index 3fd1ecd281..b22412e9d8 100644 --- a/chrome/content/zotero/components/icons.jsx +++ b/chrome/content/zotero/components/icons.jsx @@ -80,6 +80,22 @@ i('Twisty', ( )); +i('ArrowLeft', ( + /* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + + +)); +i('ArrowRight', ( + /* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + + +)); i('Cross', "chrome://zotero/skin/cross.png"); i('Tick', "chrome://zotero/skin/tick.png"); i('ArrowRefresh', "chrome://zotero/skin/arrow_refresh.png"); diff --git a/chrome/content/zotero/components/tabBar.jsx b/chrome/content/zotero/components/tabBar.jsx index d1c50bda40..e8659cea59 100644 --- a/chrome/content/zotero/components/tabBar.jsx +++ b/chrome/content/zotero/components/tabBar.jsx @@ -25,9 +25,11 @@ 'use strict'; -import React, { forwardRef, useState, useRef, useImperativeHandle, useLayoutEffect } from 'react'; +import React, { forwardRef, useState, useRef, useImperativeHandle, useEffect, useLayoutEffect } from 'react'; import cx from 'classnames'; -const { IconXmark } = require('./icons'); +const { IconXmark, IconArrowLeft, IconArrowRight } = require('./icons'); + +const SCROLL_ARROW_SCROLL_BY = 200; const TabBar = forwardRef(function (props, ref) { const [tabs, setTabs] = useState([]); @@ -36,11 +38,23 @@ const TabBar = forwardRef(function (props, ref) { const dragIDRef = useRef(null); const dragGrabbedDeltaXRef = useRef(); const tabsRef = useRef(); + const startArrowRef = useRef(); + const endArrowRef = useRef(); // Used to throttle mouse movement const mouseMoveWaitUntil = useRef(0); useImperativeHandle(ref, () => ({ setTabs })); - + + useEffect(() => { + let handleResize = () => updateScrollArrows(); + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + useLayoutEffect(() => updateScrollArrows()); + // Use offsetLeft and offsetWidth to calculate and translate tab X position useLayoutEffect(() => { if (!dragIDRef.current) return; @@ -69,6 +83,34 @@ const TabBar = forwardRef(function (props, ref) { tab.style.transform = dragging ? `translateX(${x}px)` : 'unset'; } }); + + function updateScrollArrows() { + let enableArrows = tabsRef.current.scrollWidth !== tabsRef.current.clientWidth; + if (enableArrows) { + startArrowRef.current.classList.add('enabled'); + endArrowRef.current.classList.add('enabled'); + } + else { + startArrowRef.current.classList.remove('enabled'); + endArrowRef.current.classList.remove('enabled'); + } + + if (enableArrows) { + if (tabsRef.current.scrollLeft !== 0) { + startArrowRef.current.classList.add('active'); + } + else { + startArrowRef.current.classList.remove('active'); + } + + if (tabsRef.current.scrollWidth - tabsRef.current.clientWidth !== Math.abs(tabsRef.current.scrollLeft)) { + endArrowRef.current.classList.add('active'); + } + else { + endArrowRef.current.classList.remove('active'); + } + } + } function handleTabMouseDown(event, id) { if (event.button === 2) { @@ -194,6 +236,41 @@ const TabBar = forwardRef(function (props, ref) { window.Zotero_Tooltip.stop(); } + function handleWheel(event) { + // Normalize wheel speed + let x = event.deltaX; + if (x && event.deltaMode) { + if (event.deltaMode === 1) { + x *= 20; + } + else { + x *= 400; + } + } + window.requestAnimationFrame(() => { + tabsRef.current.scrollLeft += x; + }); + } + + function handleClickScrollStart() { + tabsRef.current.scrollTo({ + left: tabsRef.current.scrollLeft - (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)), + behavior: 'smooth' + }); + } + + function handleClickScrollEnd() { + tabsRef.current.scrollTo({ + left: tabsRef.current.scrollLeft + (SCROLL_ARROW_SCROLL_BY * (Zotero.rtl ? -1 : 1)), + behavior: 'smooth' + }); + } + + // Prevent maximizing/minimizing window + function handleScrollArrowDoubleClick(event) { + event.preventDefault(); + } + function renderTab({ id, title, selected }, index) { return (
- {tabs.map((tab, index) => renderTab(tab, index))} +
+
+
+
+
+ {tabs.map((tab, index) => renderTab(tab, index))} +
+
+
+
); }); diff --git a/chrome/content/zotero/tabs.js b/chrome/content/zotero/tabs.js index 39cf709cc5..1c0eb7cea9 100644 --- a/chrome/content/zotero/tabs.js +++ b/chrome/content/zotero/tabs.js @@ -312,6 +312,19 @@ var Zotero_Tabs = new function () { } tab.lastFocusedElement = null; } + let tabNode = document.querySelector(`.tab[data-id="${tab.id}"]`); + let tabsContainerNode = document.querySelector('#tab-bar-container .tabs'); + document.querySelector(`.tab[data-id="${tab.id}"]`).scrollIntoView({ behavior: 'smooth' }); + // Border is not included when scrolling element node into view, therefore we do it manually. + // TODO: `scroll-padding` since Firefox 68 can probably be used instead + setTimeout(() => { + if (tabNode.offsetLeft + tabNode.offsetWidth - tabsContainerNode.offsetWidth + 1 >= tabsContainerNode.scrollLeft) { + document.querySelector('#tab-bar-container .tabs').scrollLeft += 1; + } + else if (tabNode.offsetLeft - 1 <= tabsContainerNode.scrollLeft) { + document.querySelector('#tab-bar-container .tabs').scrollLeft -= 1; + } + }, 500); }; /** diff --git a/scss/components/_tabBar.scss b/scss/components/_tabBar.scss index d4bf91002b..e8b7dd711f 100644 --- a/scss/components/_tabBar.scss +++ b/scss/components/_tabBar.scss @@ -2,8 +2,62 @@ min-height: 30px; } +.tab-bar-inner-container { + display: flex; + + .tabs-wrapper { + position: relative; + flex-grow: 1; + } + + .scroll-start-arrow, .scroll-end-arrow { + height: 30px; + padding: 0 3px; + align-items: center; + z-index: 1; + color: #bebebe; + display: none; + box-shadow: none; + + &.enabled { + display: flex; + } + + .icon { + display: flex; + } + + &.active { + color: #505050; + &:hover { + background-color: rgba(0, 0, 0, 0.08); + } + } + } + + .scroll-start-arrow { + border-right: 1px solid transparent; + &.active { + border-right: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: 1px 0 0 0 rgba(0,0,0,0.05); + } + } + + .scroll-end-arrow { + border-left: 1px solid transparent; + &.active { + border-left: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: -1px 0 0 0 rgba(0,0,0,0.05); + } + } +} + .tabs { display: flex; + position: absolute; + overflow: hidden; + left: 0; + right: 0; &:before { content: ""; @@ -36,6 +90,7 @@ color: #000; text-align: center; padding: 0 30px; + min-width: 100px; &:not(:last-child) { border-inline-end: $tab-border; @@ -73,6 +128,10 @@ text-align: center; line-height: 16px; border-radius: 3px; + + .icon { + display: flex; + } &:hover { background-color: rgba(0, 0, 0, 0.08); diff --git a/scss/mac/_tabBar.scss b/scss/mac/_tabBar.scss index 43fabae76f..5914b9d49a 100644 --- a/scss/mac/_tabBar.scss +++ b/scss/mac/_tabBar.scss @@ -6,13 +6,7 @@ -moz-window-dragging: no-drag; } -.tabs { - &:before { - width: 78px; - min-width: 78px; - } - - &:after { - min-width: 20px; - } +.tab-bar-inner-container { + margin-inline-start: 78px; + margin-inline-end: 20px; }