Make tab bar scrollable (#2506)

Fixes #2232
This commit is contained in:
Martynas Bagdonas 2022-04-20 05:51:32 +07:00 committed by GitHub
parent 7b0d6951d2
commit 5d442b706e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 199 additions and 19 deletions

View file

@ -80,6 +80,22 @@ i('Twisty', (
<path d="M8 13.4c-.5 0-.9-.2-1.2-.6L.4 5.2C0 4.7-.1 4.3.2 3.7S1 3 1.6 3h12.8c.6 0 1.2.1 1.4.7.3.6.2 1.1-.2 1.6l-6.4 7.6c-.3.4-.7.5-1.2.5z"/>
</svg>
));
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/. */
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="m5.001 8.352 5.465 5.466a.626.626 0 0 0 .884-.886L6.416 7.999l4.933-4.932a.626.626 0 0 0-.885-.885L5 7.647l.001.705z"/>
</svg>
));
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/. */
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="m10.999 8.352-5.465 5.466a.626.626 0 0 1-.884-.886l4.935-4.934-4.934-4.931a.626.626 0 0 1 .885-.885L11 7.647l-.001.705z"/>
</svg>
));
i('Cross', "chrome://zotero/skin/cross.png");
i('Tick', "chrome://zotero/skin/tick.png");
i('ArrowRefresh', "chrome://zotero/skin/arrow_refresh.png");

View file

@ -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 (
<div
@ -219,13 +296,34 @@ const TabBar = forwardRef(function (props, ref) {
}
return (
<div
ref={tabsRef}
className="tabs"
onDragOver={handleTabBarDragOver}
onMouseOut={handleTabBarMouseOut}
>
{tabs.map((tab, index) => renderTab(tab, index))}
<div>
<div className="tab-bar-inner-container" onWheel={handleWheel}>
<div
ref={startArrowRef}
className="scroll-start-arrow"
style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }}
onClick={handleClickScrollStart}
onDoubleClick={handleScrollArrowDoubleClick}
><IconArrowLeft/></div>
<div className="tabs-wrapper">
<div
ref={tabsRef}
className="tabs"
onDragOver={handleTabBarDragOver}
onMouseOut={handleTabBarMouseOut}
onScroll={updateScrollArrows}
>
{tabs.map((tab, index) => renderTab(tab, index))}
</div>
</div>
<div
ref={endArrowRef}
className="scroll-end-arrow"
style={{ transform: Zotero.rtl ? 'scaleX(-1)' : undefined }}
onClick={handleClickScrollEnd}
onDoubleClick={handleScrollArrowDoubleClick}
><IconArrowRight/></div>
</div>
</div>
);
});

View file

@ -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);
};
/**

View file

@ -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);

View file

@ -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;
}