diff --git a/src/components/FloatingButton/FloatingButton.tsx b/src/components/FloatingButton/FloatingButton.tsx index 42d2516..a832410 100644 --- a/src/components/FloatingButton/FloatingButton.tsx +++ b/src/components/FloatingButton/FloatingButton.tsx @@ -15,10 +15,28 @@ export function FloatingButton() { const { showScrollTop } = useScrollHandler(); const scrollToTop = useCallback(() => { - window.scrollTo({ - top: 0, - behavior: "smooth", - }); + // Use a more stable approach to find the scroll container + const findScrollContainer = () => { + const antApp = document.querySelector('.ant-app') as HTMLElement; + if (antApp && antApp.scrollHeight > antApp.clientHeight) { + return antApp; + } + + const appContainer = document.querySelector('[class*="App"]') as HTMLElement; + if (appContainer && appContainer.scrollHeight > appContainer.clientHeight) { + return appContainer; + } + + return document.documentElement; + }; + + const scrollContainer = findScrollContainer(); + if (scrollContainer) { + scrollContainer.scrollTo({ + top: 0, + behavior: "smooth", + }); + } }, []); return ( diff --git a/src/contexts/ScrollHandlerContext.tsx b/src/contexts/ScrollHandlerContext.tsx index ad0b74d..8d3d87f 100644 --- a/src/contexts/ScrollHandlerContext.tsx +++ b/src/contexts/ScrollHandlerContext.tsx @@ -41,10 +41,36 @@ export function ScrollHandlerProvider({ children }: { children: ReactNode }) { // Function to scroll to a specific category const scrollToCategory = useCallback((categoryId: number) => { const categoryRef = categoryRefs.current?.[categoryId]; - if (categoryRef) { - categoryRef.scrollIntoView({ - behavior: "smooth", - block: "start", + + // Use a more stable approach to find the scroll container + const findScrollContainer = () => { + const antApp = document.querySelector('.ant-app') as HTMLElement; + if (antApp && antApp.scrollHeight > antApp.clientHeight) { + return antApp; + } + + const appContainer = document.querySelector('[class*="App"]') as HTMLElement; + if (appContainer && appContainer.scrollHeight > appContainer.clientHeight) { + return appContainer; + } + + return document.documentElement; + }; + + const scrollContainer = findScrollContainer(); + + if (categoryRef && scrollContainer) { + // Get the position of the category relative to the scroll container + const categoryRect = categoryRef.getBoundingClientRect(); + const containerRect = scrollContainer.getBoundingClientRect(); + + // Calculate the target scroll position + const targetScrollTop = scrollContainer.scrollTop + categoryRect.top - containerRect.top; + + // Smooth scroll to the target position + scrollContainer.scrollTo({ + top: targetScrollTop, + behavior: "smooth" }); } }, []); diff --git a/src/pages/menu/components/ScrollEventHandler.tsx b/src/pages/menu/components/ScrollEventHandler.tsx index 4003a2d..b60cddd 100644 --- a/src/pages/menu/components/ScrollEventHandler.tsx +++ b/src/pages/menu/components/ScrollEventHandler.tsx @@ -1,21 +1,58 @@ import { useScrollHandler } from "contexts/ScrollHandlerContext"; -import { useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; export default function ScrollEventHandler() { - const { - setShowScrollTop, - setIsCategoriesSticky, + const { + setShowScrollTop, + setIsCategoriesSticky, categoriesContainerRef, categoryRefs, - activeCategory, - setActiveCategory + setActiveCategory, } = useScrollHandler(); const hasSetInitialCategory = useRef(false); + const scrollContainerRef = useRef(null); + const lastStickyState = useRef(false); + const originalCategoriesTop = useRef(null); + + // Find the main scrollable container - use a more stable approach + useEffect(() => { + // Use a more stable approach - find the container once and cache it + const findScrollContainer = () => { + // Try to find the Ant Design app container first + const antApp = document.querySelector(".ant-app") as HTMLElement; + if (antApp && antApp.scrollHeight > antApp.clientHeight) { + return antApp; + } + + // Fallback to any container with App in the class name + const appContainer = document.querySelector( + '[class*="App"]', + ) as HTMLElement; + if ( + appContainer && + appContainer.scrollHeight > appContainer.clientHeight + ) { + return appContainer; + } + + // Last resort - use document element + return document.documentElement; + }; + + const scrollContainer = findScrollContainer(); + if (scrollContainer) { + scrollContainerRef.current = scrollContainer; + } + }, []); // Set initial active category when categories are available useEffect(() => { - if (categoryRefs.current && Object.keys(categoryRefs.current).length > 0 && !hasSetInitialCategory.current) { + if ( + categoryRefs.current && + Object.keys(categoryRefs.current).length > 0 && + !hasSetInitialCategory.current + ) { // Set the first category as active initially const firstCategoryId = parseInt(Object.keys(categoryRefs.current)[0]); setActiveCategory(firstCategoryId); @@ -23,75 +60,112 @@ export default function ScrollEventHandler() { } }, [categoryRefs, setActiveCategory]); + // Store the original position of categories container once useEffect(() => { - const handleScroll = () => { - const scrollTop = - window.pageYOffset || document.documentElement.scrollTop; - setShowScrollTop(scrollTop > 300); // Show button after scrolling 300px + if ( + categoriesContainerRef.current && + originalCategoriesTop.current === null + ) { + const rect = categoriesContainerRef.current.getBoundingClientRect(); + originalCategoriesTop.current = + rect.top + (scrollContainerRef.current?.scrollTop || 0); + } + }, [categoriesContainerRef]); - // Check if we should make categories sticky - if (categoriesContainerRef.current) { - // Get the original position of the categories container - const originalTop = categoriesContainerRef.current.offsetTop; + const handleScroll = useCallback(() => { + // Use the scrollable container instead of window + const scrollTop = scrollContainerRef.current?.scrollTop || 0; + setShowScrollTop(scrollTop > 300); // Show button after scrolling 300px - // Only make sticky when we've scrolled past the original position - // and return to normal when we scroll back above it - // Add a small threshold (10px) to prevent flickering - const shouldBeSticky = scrollTop > originalTop + 10; + // Check if we should make categories sticky + if ( + categoriesContainerRef.current && + scrollContainerRef.current && + originalCategoriesTop.current !== null + ) { + // Use the cached original position to prevent flickering + const threshold = 50; // Increased threshold to prevent flickering + const shouldBeSticky = + scrollTop > originalCategoriesTop.current + threshold; + + // Only update state if it actually changed to prevent unnecessary re-renders + if (shouldBeSticky !== lastStickyState.current) { setIsCategoriesSticky(shouldBeSticky); + lastStickyState.current = shouldBeSticky; } + } - // Find the most visible category based on scroll position - if (categoryRefs.current) { - let mostVisibleCategory: { id: number; visibility: number } | null = null; - - Object.entries(categoryRefs.current).forEach(([categoryId, element]) => { - if (element) { - const rect = element.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - - // Calculate visibility ratio - const elementTop = rect.top; - const elementBottom = rect.bottom; - const elementHeight = rect.height; - - // Calculate how much of the element is visible - let visibleHeight = 0; - if (elementTop >= 0 && elementBottom <= viewportHeight) { - // Element is fully visible - visibleHeight = elementHeight; - } else if (elementTop < 0 && elementBottom > 0) { - // Element is partially visible from top - visibleHeight = elementBottom; - } else if (elementTop < viewportHeight && elementBottom > viewportHeight) { - // Element is partially visible from bottom - visibleHeight = viewportHeight - elementTop; - } - - const visibility = visibleHeight / elementHeight; - - // Only consider elements that are at least 30% visible - if (visibility > 0.3) { - if (!mostVisibleCategory || visibility > mostVisibleCategory.visibility) { - mostVisibleCategory = { - id: parseInt(categoryId), - visibility - }; - } + // Find the most visible category based on scroll position + if (categoryRefs.current && scrollContainerRef.current) { + let mostVisibleCategory: { id: number; visibility: number } | null = null; + + Object.entries(categoryRefs.current).forEach(([categoryId, element]) => { + if (element) { + const rect = element.getBoundingClientRect(); + const containerRect = + scrollContainerRef.current!.getBoundingClientRect(); + + // Calculate visibility ratio relative to the scroll container + const elementTop = rect.top - containerRect.top; + const elementBottom = elementTop + rect.height; + const containerHeight = containerRect.height; + const elementHeight = rect.height; + + // Calculate how much of the element is visible within the container + let visibleHeight = 0; + if (elementTop >= 0 && elementBottom <= containerHeight) { + // Element is fully visible + visibleHeight = elementHeight; + } else if (elementTop < 0 && elementBottom > 0) { + // Element is partially visible from top + visibleHeight = elementBottom; + } else if ( + elementTop < containerHeight && + elementBottom > containerHeight + ) { + // Element is partially visible from bottom + visibleHeight = containerHeight - elementTop; + } + + const visibility = visibleHeight / elementHeight; + + // Only consider elements that are at least 30% visible + if (visibility > 0.3) { + if ( + !mostVisibleCategory || + visibility > mostVisibleCategory.visibility + ) { + mostVisibleCategory = { + id: parseInt(categoryId), + visibility, + }; } } - }); - - // Update active category if we found a visible one - if (mostVisibleCategory) { - setActiveCategory((mostVisibleCategory as { id: number; visibility: number }).id); } - } - }; + }); - window.addEventListener("scroll", handleScroll); - return () => window.removeEventListener("scroll", handleScroll); - }, [categoriesContainerRef, setIsCategoriesSticky, setShowScrollTop, categoryRefs, setActiveCategory, activeCategory]); + // Update active category if we found a visible one + if (mostVisibleCategory) { + setActiveCategory( + (mostVisibleCategory as { id: number; visibility: number }).id, + ); + } + } + }, [ + setShowScrollTop, + categoriesContainerRef, + categoryRefs, + setIsCategoriesSticky, + setActiveCategory, + ]); // categoryRefs and categoriesContainerRef are refs, don't need to be in deps + + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + if (scrollContainer) { + scrollContainer.addEventListener("scroll", handleScroll); + return () => scrollContainer.removeEventListener("scroll", handleScroll); + } + }, [handleScroll]); return null; } diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index 81faa06..2cb79be 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -27,11 +27,29 @@ export const ScrollToTop: React.FC = () => { const { pathname } = useLocation(); useEffect(() => { - window.scrollTo({ - top: 0, - left: 0, - behavior: "smooth", - }); // Scroll to the top when the location changes + // Use a more stable approach to find the scroll container + const findScrollContainer = () => { + const antApp = document.querySelector('.ant-app') as HTMLElement; + if (antApp && antApp.scrollHeight > antApp.clientHeight) { + return antApp; + } + + const appContainer = document.querySelector('[class*="App"]') as HTMLElement; + if (appContainer && appContainer.scrollHeight > appContainer.clientHeight) { + return appContainer; + } + + return document.documentElement; + }; + + const scrollContainer = findScrollContainer(); + if (scrollContainer) { + scrollContainer.scrollTo({ + top: 0, + left: 0, + behavior: "smooth", + }); // Scroll to the top when the location changes + } }, [pathname]); return null; // This component doesn't render anything