Fix mobile tab header and swipe behavior

This commit is contained in:
2025-12-22 21:01:33 +01:00
parent 1f52680721
commit a1ae2a53a6
6 changed files with 38 additions and 6 deletions

View File

@@ -21,6 +21,7 @@ type SwipeTabsProps<T extends string | number> = {
swipeDisabled?: boolean; swipeDisabled?: boolean;
threshold?: number; threshold?: number;
renderWindow?: number; renderWindow?: number;
scrollToTopOnChange?: boolean;
}; };
const DRAG_START_DISTANCE = 3; const DRAG_START_DISTANCE = 3;
@@ -55,7 +56,8 @@ export function SwipeTabs<T extends string | number>({
panelClassName, panelClassName,
swipeDisabled = false, swipeDisabled = false,
threshold = DEFAULT_THRESHOLD, threshold = DEFAULT_THRESHOLD,
renderWindow = DEFAULT_RENDER_WINDOW renderWindow = DEFAULT_RENDER_WINDOW,
scrollToTopOnChange = true
}: SwipeTabsProps<T>) { }: SwipeTabsProps<T>) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null); const trackRef = useRef<HTMLDivElement>(null);
@@ -77,6 +79,7 @@ export function SwipeTabs<T extends string | number>({
const pendingIndexRef = useRef<number | null>(null); const pendingIndexRef = useRef<number | null>(null);
const isAnimatingRef = useRef(false); const isAnimatingRef = useRef(false);
const needsResetRef = useRef(false); const needsResetRef = useRef(false);
const hasScrolledRef = useRef(false);
const dragRef = useRef({ const dragRef = useRef({
pointerId: null as number | null, pointerId: null as number | null,
touchId: null as number | null, touchId: null as number | null,
@@ -164,6 +167,16 @@ export function SwipeTabs<T extends string | number>({
}; };
}, []); }, []);
useEffect(() => {
if (!scrollToTopOnChange) return;
if (!hasScrolledRef.current) {
hasScrolledRef.current = true;
return;
}
if (typeof window === 'undefined') return;
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
}, [activeTab, scrollToTopOnChange]);
const resetToIndex = useCallback( const resetToIndex = useCallback(
(index: number) => { (index: number) => {
displayIndexRef.current = index; displayIndexRef.current = index;

View File

@@ -16,6 +16,7 @@ type TabsScrollerProps = {
scrollStep?: number; scrollStep?: number;
ariaLabelLeft?: string; ariaLabelLeft?: string;
ariaLabelRight?: string; ariaLabelRight?: string;
showArrows?: boolean;
}; };
const SCROLL_EDGE_THRESHOLD = 8; const SCROLL_EDGE_THRESHOLD = 8;
@@ -30,7 +31,8 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
children, children,
scrollStep, scrollStep,
ariaLabelLeft = 'Scroll tabs left', ariaLabelLeft = 'Scroll tabs left',
ariaLabelRight = 'Scroll tabs right' ariaLabelRight = 'Scroll tabs right',
showArrows = false
}, },
ref ref
) => { ) => {
@@ -63,10 +65,21 @@ const TabsScroller = forwardRef<HTMLDivElement, TabsScrollerProps>(
const updateOverflow = useCallback(() => { const updateOverflow = useCallback(() => {
const node = sliderRef.current; const node = sliderRef.current;
if (!node) return; if (!node) return;
if (!showArrows) {
setShowLeft(false);
setShowRight(false);
return;
}
const hasTabs = Boolean(node.querySelector('.page-tab-btn, .admin-tab-btn'));
if (!hasTabs) {
setShowLeft(false);
setShowRight(false);
return;
}
const maxScroll = node.scrollWidth - node.clientWidth; const maxScroll = node.scrollWidth - node.clientWidth;
setShowLeft(node.scrollLeft > SCROLL_EDGE_THRESHOLD); setShowLeft(node.scrollLeft > SCROLL_EDGE_THRESHOLD);
setShowRight(node.scrollLeft < maxScroll - SCROLL_EDGE_THRESHOLD); setShowRight(node.scrollLeft < maxScroll - SCROLL_EDGE_THRESHOLD);
}, []); }, [showArrows]);
const scheduleOverflowUpdate = useCallback(() => { const scheduleOverflowUpdate = useCallback(() => {
if (rafRef.current) return; if (rafRef.current) return;

View File

@@ -25,7 +25,7 @@ export default function AdminPanel({ initialTab = 'general' }: { initialTab?: Ta
return ( return (
<main className="main-content admin-panel-root"> <main className="main-content admin-panel-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider" showArrows>
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span> <span className="material-symbols-outlined">menu</span>
</button> </button>

View File

@@ -744,7 +744,7 @@ export default function Features() {
return ( return (
<main className="main-content admin-panel-root"> <main className="main-content admin-panel-root">
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider" ref={tabsContainerRef}> <TabsScroller className="page-tabs-slider" ref={tabsContainerRef} showArrows>
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span> <span className="material-symbols-outlined">menu</span>
</button> </button>

View File

@@ -661,7 +661,7 @@ export default function ThemeSettings() {
)} )}
{/* Modern Tab Navigation */} {/* Modern Tab Navigation */}
<div className="page-tabs-container"> <div className="page-tabs-container">
<TabsScroller className="page-tabs-slider"> <TabsScroller className="page-tabs-slider" showArrows>
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}> <button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
<span className="material-symbols-outlined">menu</span> <span className="material-symbols-outlined">menu</span>
</button> </button>

View File

@@ -464,6 +464,10 @@ body {
min-height: 48px; min-height: 48px;
} }
.tabs-scroll-shell {
width: 100%;
}
.page-title-section, .page-title-section,
.admin-title-section { .admin-title-section {
display: flex; display: flex;
@@ -609,6 +613,8 @@ body {
width: 100%; width: 100%;
max-width: none; max-width: none;
margin: 0; margin: 0;
flex: 1 1 auto;
min-height: 0;
} }
/* Swipeable tab panels */ /* Swipeable tab panels */