增加顶部标签栏左右滚动控制
This commit is contained in:
@@ -33,6 +33,15 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-tabs__rail {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-items: flex-end;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-tabs__track {
|
.workspace-tabs__track {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@@ -41,6 +50,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
scroll-behavior: smooth;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +96,7 @@
|
|||||||
|
|
||||||
.workspace-tab__main,
|
.workspace-tab__main,
|
||||||
.workspace-tab__close,
|
.workspace-tab__close,
|
||||||
|
.workspace-tabs__scroll,
|
||||||
.workspace-tabs__more,
|
.workspace-tabs__more,
|
||||||
.workspace-tabs__theme,
|
.workspace-tabs__theme,
|
||||||
.workspace-tabs__logout,
|
.workspace-tabs__logout,
|
||||||
@@ -131,6 +143,7 @@
|
|||||||
|
|
||||||
.workspace-tab__main:focus-visible,
|
.workspace-tab__main:focus-visible,
|
||||||
.workspace-tab__close:focus-visible,
|
.workspace-tab__close:focus-visible,
|
||||||
|
.workspace-tabs__scroll:focus-visible,
|
||||||
.workspace-tabs__more:focus-visible,
|
.workspace-tabs__more:focus-visible,
|
||||||
.workspace-tabs__theme:focus-visible,
|
.workspace-tabs__theme:focus-visible,
|
||||||
.workspace-tabs__logout:focus-visible,
|
.workspace-tabs__logout:focus-visible,
|
||||||
@@ -139,6 +152,29 @@
|
|||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-tabs__scroll {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 28px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--ant-color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-tabs__scroll:not(:disabled):hover {
|
||||||
|
color: var(--ant-color-primary);
|
||||||
|
background: var(--ant-color-fill-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-tabs__scroll:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.28;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-tabs__more,
|
.workspace-tabs__more,
|
||||||
.workspace-tabs__theme,
|
.workspace-tabs__theme,
|
||||||
.workspace-tabs__logout {
|
.workspace-tabs__logout {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import { Dropdown, Tooltip } from 'antd';
|
import { Dropdown, Tooltip } from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
@@ -11,10 +11,12 @@ import {
|
|||||||
DownOutlined,
|
DownOutlined,
|
||||||
IdcardOutlined,
|
IdcardOutlined,
|
||||||
KeyOutlined,
|
KeyOutlined,
|
||||||
|
LeftOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
MoonFilled,
|
MoonFilled,
|
||||||
MoonOutlined,
|
MoonOutlined,
|
||||||
|
RightOutlined,
|
||||||
ShoppingCartOutlined,
|
ShoppingCartOutlined,
|
||||||
SunOutlined,
|
SunOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -90,6 +92,13 @@ export default function WorkspaceTabs() {
|
|||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const activePath = normalizePath(pathname);
|
const activePath = normalizePath(pathname);
|
||||||
const [openedPaths, setOpenedPaths] = useState(() => readOpenedPaths(activePath));
|
const [openedPaths, setOpenedPaths] = useState(() => readOpenedPaths(activePath));
|
||||||
|
const tabsRailRef = useRef<HTMLDivElement>(null);
|
||||||
|
const tabsTrackRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [tabScrollState, setTabScrollState] = useState({
|
||||||
|
hasOverflow: false,
|
||||||
|
canScrollLeft: false,
|
||||||
|
canScrollRight: false,
|
||||||
|
});
|
||||||
|
|
||||||
const openedTabs = useMemo(
|
const openedTabs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -105,10 +114,69 @@ export default function WorkspaceTabs() {
|
|||||||
setOpenedPaths(paths);
|
setOpenedPaths(paths);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const updateTabScrollState = useCallback(() => {
|
||||||
|
const rail = tabsRailRef.current;
|
||||||
|
const track = tabsTrackRef.current;
|
||||||
|
if (!rail || !track) return;
|
||||||
|
|
||||||
|
const maxScrollLeft = Math.max(0, track.scrollWidth - track.clientWidth);
|
||||||
|
const hasOverflow = track.scrollWidth > rail.clientWidth + 1;
|
||||||
|
const nextState = {
|
||||||
|
hasOverflow,
|
||||||
|
canScrollLeft: hasOverflow && track.scrollLeft > 1,
|
||||||
|
canScrollRight: hasOverflow && track.scrollLeft < maxScrollLeft - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTabScrollState((currentState) =>
|
||||||
|
currentState.hasOverflow === nextState.hasOverflow &&
|
||||||
|
currentState.canScrollLeft === nextState.canScrollLeft &&
|
||||||
|
currentState.canScrollRight === nextState.canScrollRight
|
||||||
|
? currentState
|
||||||
|
: nextState,
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollTabs = useCallback((direction: 'left' | 'right') => {
|
||||||
|
const track = tabsTrackRef.current;
|
||||||
|
if (!track) return;
|
||||||
|
|
||||||
|
const distance = Math.max(180, Math.round(track.clientWidth * 0.7));
|
||||||
|
track.scrollBy({
|
||||||
|
left: direction === 'left' ? -distance : distance,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
saveOpenedPaths(openedPaths);
|
saveOpenedPaths(openedPaths);
|
||||||
}, [openedPaths]);
|
}, [openedPaths]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const rail = tabsRailRef.current;
|
||||||
|
const track = tabsTrackRef.current;
|
||||||
|
if (!rail || !track) return;
|
||||||
|
|
||||||
|
updateTabScrollState();
|
||||||
|
|
||||||
|
const resizeObserver =
|
||||||
|
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateTabScrollState);
|
||||||
|
resizeObserver?.observe(rail);
|
||||||
|
resizeObserver?.observe(track);
|
||||||
|
window.addEventListener('resize', updateTabScrollState);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
window.removeEventListener('resize', updateTabScrollState);
|
||||||
|
};
|
||||||
|
}, [openedTabs, updateTabScrollState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const track = tabsTrackRef.current;
|
||||||
|
const activeTabElement = track?.querySelector<HTMLElement>('.workspace-tab.is-active');
|
||||||
|
activeTabElement?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
||||||
|
updateTabScrollState();
|
||||||
|
}, [activePath, updateTabScrollState]);
|
||||||
|
|
||||||
const closeTab = useCallback(
|
const closeTab = useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
const target = definitionByPath.get(path);
|
const target = definitionByPath.get(path);
|
||||||
@@ -247,7 +315,28 @@ export default function WorkspaceTabs() {
|
|||||||
<h1 className="workspace-tabs__page-title">{activeTab.title}</h1>
|
<h1 className="workspace-tabs__page-title">{activeTab.title}</h1>
|
||||||
|
|
||||||
<div className="workspace-tabs__desktop">
|
<div className="workspace-tabs__desktop">
|
||||||
<div className="workspace-tabs__track" role="tablist" aria-label="已打开页面">
|
<div ref={tabsRailRef} className="workspace-tabs__rail">
|
||||||
|
{tabScrollState.hasOverflow && (
|
||||||
|
<Tooltip title="向左滚动">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="workspace-tabs__scroll"
|
||||||
|
aria-label="向左滚动已打开页面"
|
||||||
|
disabled={!tabScrollState.canScrollLeft}
|
||||||
|
onClick={() => scrollTabs('left')}
|
||||||
|
>
|
||||||
|
<LeftOutlined />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={tabsTrackRef}
|
||||||
|
className="workspace-tabs__track"
|
||||||
|
role="tablist"
|
||||||
|
aria-label="已打开页面"
|
||||||
|
onScroll={updateTabScrollState}
|
||||||
|
>
|
||||||
{openedTabs.map((tab) => {
|
{openedTabs.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
const active = tab.path === activePath;
|
const active = tab.path === activePath;
|
||||||
@@ -281,6 +370,21 @@ export default function WorkspaceTabs() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{tabScrollState.hasOverflow && (
|
||||||
|
<Tooltip title="向右滚动">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="workspace-tabs__scroll"
|
||||||
|
aria-label="向右滚动已打开页面"
|
||||||
|
disabled={!tabScrollState.canScrollRight}
|
||||||
|
onClick={() => scrollTabs('right')}
|
||||||
|
>
|
||||||
|
<RightOutlined />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Dropdown menu={{ items: actionItems, onClick: handleAction }} trigger={['click']}>
|
<Dropdown menu={{ items: actionItems, onClick: handleAction }} trigger={['click']}>
|
||||||
<Tooltip title="标签操作">
|
<Tooltip title="标签操作">
|
||||||
<button type="button" className="workspace-tabs__more" aria-label="标签操作">
|
<button type="button" className="workspace-tabs__more" aria-label="标签操作">
|
||||||
|
|||||||
Reference in New Issue
Block a user