Fix mobile tab header and swipe behavior
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user