增加顶部标签栏左右滚动控制
This commit is contained in:
@@ -33,6 +33,15 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
@@ -41,6 +50,8 @@
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior-x: contain;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@@ -85,6 +96,7 @@
|
||||
|
||||
.workspace-tab__main,
|
||||
.workspace-tab__close,
|
||||
.workspace-tabs__scroll,
|
||||
.workspace-tabs__more,
|
||||
.workspace-tabs__theme,
|
||||
.workspace-tabs__logout,
|
||||
@@ -131,6 +143,7 @@
|
||||
|
||||
.workspace-tab__main:focus-visible,
|
||||
.workspace-tab__close:focus-visible,
|
||||
.workspace-tabs__scroll:focus-visible,
|
||||
.workspace-tabs__more:focus-visible,
|
||||
.workspace-tabs__theme:focus-visible,
|
||||
.workspace-tabs__logout:focus-visible,
|
||||
@@ -139,6 +152,29 @@
|
||||
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__theme,
|
||||
.workspace-tabs__logout {
|
||||
|
||||
+133
-29
@@ -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 { Dropdown, Tooltip } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
DownOutlined,
|
||||
IdcardOutlined,
|
||||
KeyOutlined,
|
||||
LeftOutlined,
|
||||
LogoutOutlined,
|
||||
MoreOutlined,
|
||||
MoonFilled,
|
||||
MoonOutlined,
|
||||
RightOutlined,
|
||||
ShoppingCartOutlined,
|
||||
SunOutlined,
|
||||
UserOutlined,
|
||||
@@ -90,6 +92,13 @@ export default function WorkspaceTabs() {
|
||||
const { pathname } = useLocation();
|
||||
const activePath = normalizePath(pathname);
|
||||
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(
|
||||
() =>
|
||||
@@ -105,10 +114,69 @@ export default function WorkspaceTabs() {
|
||||
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(() => {
|
||||
saveOpenedPaths(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(
|
||||
(path: string) => {
|
||||
const target = definitionByPath.get(path);
|
||||
@@ -247,38 +315,74 @@ export default function WorkspaceTabs() {
|
||||
<h1 className="workspace-tabs__page-title">{activeTab.title}</h1>
|
||||
|
||||
<div className="workspace-tabs__desktop">
|
||||
<div className="workspace-tabs__track" role="tablist" aria-label="已打开页面">
|
||||
{openedTabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const active = tab.path === activePath;
|
||||
return (
|
||||
<div key={tab.path} className={`workspace-tab${active ? ' is-active' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="workspace-tab__main"
|
||||
role="tab"
|
||||
aria-label={tab.title}
|
||||
aria-selected={active}
|
||||
title={tab.title}
|
||||
onClick={() => navigate(tab.path)}
|
||||
>
|
||||
<Icon />
|
||||
<span className="workspace-tab__label">{tab.title}</span>
|
||||
</button>
|
||||
{!tab.pinned && (
|
||||
<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) => {
|
||||
const Icon = tab.icon;
|
||||
const active = tab.path === activePath;
|
||||
return (
|
||||
<div key={tab.path} className={`workspace-tab${active ? ' is-active' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="workspace-tab__close"
|
||||
aria-label={`关闭 ${tab.title}`}
|
||||
title={`关闭 ${tab.title}`}
|
||||
onClick={() => closeTab(tab.path)}
|
||||
className="workspace-tab__main"
|
||||
role="tab"
|
||||
aria-label={tab.title}
|
||||
aria-selected={active}
|
||||
title={tab.title}
|
||||
onClick={() => navigate(tab.path)}
|
||||
>
|
||||
<CloseOutlined />
|
||||
<Icon />
|
||||
<span className="workspace-tab__label">{tab.title}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!tab.pinned && (
|
||||
<button
|
||||
type="button"
|
||||
className="workspace-tab__close"
|
||||
aria-label={`关闭 ${tab.title}`}
|
||||
title={`关闭 ${tab.title}`}
|
||||
onClick={() => closeTab(tab.path)}
|
||||
>
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
)}
|
||||
</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']}>
|
||||
|
||||
Reference in New Issue
Block a user