diff --git a/src/components/swipeable-tabs.tsx b/src/components/swipeable-tabs.tsx index d947305..cab016a 100644 --- a/src/components/swipeable-tabs.tsx +++ b/src/components/swipeable-tabs.tsx @@ -1,4 +1,4 @@ -import { FloatingIndicator, UnstyledButton, Box, Text, ScrollArea } from "@mantine/core"; +import { FloatingIndicator, UnstyledButton, Box, Text } from "@mantine/core"; import { Carousel } from "@mantine/carousel"; import { useState, useEffect, ReactNode, useRef } from "react"; import { useRouter } from "@tanstack/react-router"; @@ -12,23 +12,14 @@ interface SwipeableTabsProps { tabs: TabItem[]; defaultTab?: number; onTabChange?: (index: number, tab: TabItem) => void; - scrollPosition?: { x: number; y: number }; } -function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: SwipeableTabsProps) { +function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps) { const router = useRouter(); const search = router.state.location.search as any; - - // manually update url tab w/o useNavigate - const updateUrlTab = (tabLabel: string) => { - if (typeof window === 'undefined') return; - - const url = new URL(window.location.href); - url.searchParams.set('tab', tabLabel); - window.history.replaceState(null, '', url.toString()); - }; + const [embla, setEmbla] = useState(null); - const getInitialTab = () => { + const getActiveTabFromUrl = () => { const urlTab = search?.tab; if (typeof urlTab === 'string') { const tabIndex = tabs.findIndex(tab => tab.label.toLowerCase() === urlTab.toLowerCase()); @@ -37,109 +28,55 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: Sw return defaultTab; }; - const [activeTab, setActiveTab] = useState(getInitialTab); - const [embla, setEmbla] = useState(null); + const [activeTab, setActiveTab] = useState(getActiveTabFromUrl); const [rootRef, setRootRef] = useState(null); const [controlsRefs, setControlsRefs] = useState>({}); - const [isSticky, setIsSticky] = useState(false); - const tabsRef = useRef(null); - const originalPositionRef = useRef(null); const slideRefs = useRef>({}); - const [activeSlideHeight, setActiveSlideHeight] = useState('auto'); - const stickyThreshold = 0; + const [carouselHeight, setCarouselHeight] = useState('auto'); + + const changeTab = (index: number) => { + if (index === activeTab || index < 0 || index >= tabs.length) return; + + setActiveTab(index); + embla?.scrollTo(index); + onTabChange?.(index, tabs[index]); + + const tabLabel = tabs[index].label.toLowerCase(); + if (typeof window !== 'undefined') { + const url = new URL(window.location.href); + url.searchParams.set('tab', tabLabel); + window.history.replaceState(null, '', url.toString()); + } + }; useEffect(() => { if (!embla) return; const onSelect = () => { const newIndex = embla.selectedScrollSnap(); - setActiveTab(newIndex); - - const activeSlideRef = slideRefs.current[newIndex]; - if (activeSlideRef) { - const height = activeSlideRef.scrollHeight; - setActiveSlideHeight(height); - } - - const tabLabel = tabs[newIndex]?.label.toLowerCase(); - if (tabLabel) { - updateUrlTab(tabLabel); - } - - onTabChange?.(newIndex, tabs[newIndex]); + changeTab(newIndex); }; embla.on("select", onSelect); - - return () => { - embla.off("select", onSelect); - }; - }, [embla]); + return () => embla.off("select", onSelect); + }, [embla, activeTab, tabs]); + + useEffect(() => { + const newActiveTab = getActiveTabFromUrl(); + if (newActiveTab !== activeTab) { + setActiveTab(newActiveTab); + embla?.scrollTo(newActiveTab); + } + }, [search?.tab]); useEffect(() => { const activeSlideRef = slideRefs.current[activeTab]; if (activeSlideRef) { const height = activeSlideRef.scrollHeight; - setActiveSlideHeight(height); + setCarouselHeight(height); } }, [activeTab]); - useEffect(() => { - const urlTab = search?.tab; - if (typeof urlTab === 'string') { - const tabIndex = tabs.findIndex(tab => tab.label.toLowerCase() === urlTab.toLowerCase()); - if (tabIndex !== -1 && tabIndex !== activeTab) { - setActiveTab(tabIndex); - embla?.scrollTo(tabIndex); - } - } - }, [search?.tab, tabs, activeTab, embla]); - - useEffect(() => { - if (scrollPosition) { - setIsSticky(scrollPosition.y > stickyThreshold); - return; - } - - const scrollWrapper = document.getElementById('scroll-wrapper'); - let viewport = scrollWrapper!.querySelector('.mantine-ScrollArea-viewport') as HTMLElement; - - const handleScroll = () => { - if (!tabsRef.current) return; - - const scrollTop = viewport.scrollTop; - const viewportRect = viewport.getBoundingClientRect(); - - if (originalPositionRef.current === null && !isSticky) { - const tabsRect = tabsRef.current.getBoundingClientRect(); - originalPositionRef.current = tabsRect.top - viewportRect.top + scrollTop; - } - - setIsSticky( - originalPositionRef.current !== null - && scrollTop >= (originalPositionRef.current - viewportRect.top - stickyThreshold) - ); - }; - - handleScroll(); - viewport.addEventListener('scroll', handleScroll, { passive: true }); - - return () => { - viewport.removeEventListener('scroll', handleScroll); - }; - }, [stickyThreshold, isSticky, scrollPosition]); - - const handleTabChange = (index: number) => { - if (index !== activeTab && index >= 0 && index < tabs.length) { - setActiveTab(index); - embla?.scrollTo(index); - onTabChange?.(index, tabs[index]); - - const tabLabel = tabs[index].label.toLowerCase(); - updateUrlTab(tabLabel); - } - }; - const setControlRef = (index: number) => (node: HTMLSpanElement | null) => { controlsRefs[index] = node; setControlsRefs(controlsRefs); @@ -151,24 +88,15 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: Sw return ( - {isSticky && ( - - )} { - setRootRef(node); - tabsRef.current = node; - }} - pos={isSticky ? "fixed" : "relative"} + ref={setRootRef} + pos="sticky" + top={0} style={{ display: 'flex', marginBottom: 'var(--mantine-spacing-md)', - top: isSticky ? 0 : 'auto', - left: isSticky ? 0 : 'auto', - right: isSticky ? 0 : 'auto', zIndex: 100, - backgroundColor: 'var(--mantine-color-body)', - transition: 'all 200ms ease', + backgroundColor: 'var(--mantine-color-body)' }} > ( handleTabChange(index)} + onClick={() => changeTab(index)} style={{ flex: 1, padding: 'var(--mantine-spacing-sm) var(--mantine-spacing-md)', @@ -193,11 +121,10 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange, scrollPosition }: Sw ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-text)', fontWeight: activeTab === index ? 600 : 400, - transition: 'color 200ms ease', + transition: 'color 200ms ease, font-weight 200ms ease', backgroundColor: 'transparent', border: 'none', borderRadius: 0, - }} > {tabs.map((tab, index) => ( - + {tab.content}