refactor sticky scroll handler
- because of making the root's child div styles as override: auto it won't be a ref for the sticky category logic
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -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<HTMLElement | null>(null);
|
||||
const lastStickyState = useRef(false);
|
||||
const originalCategoriesTop = useRef<number | null>(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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user