skip to Main Content

I’m creating a website with a nav bar that contains 5,6 buttons that when clicked navigates user to respective page. I’m using react router for that purpose. Some pages may have scroll bars and can be scrolled while some are not scrollable.

What I want to accomplish is, when there is a page that has scrollbar or if anything is being scrolled, then do nothing, just let the user scroll through the page, but if there is nothing being scrolled i.e (if user moves mouse wheel in not scrollable page or if the scroll end is reached and still moves the wheel) then navigate to the next page.

following is my base layout page which is common for all other pages. and what I tried to accomplish this behaviour.

function Layout({error}: Props) {
    const {pathname} = useLocation();
    const {data: user, isLoading} = useUser();
    const navigate = useNavigate();
    const parent = useRef<HTMLDivElement>(null);

    useEffect(() => {
        const parentDiv = parent.current;
        let scrolling = false;
        parentDiv?.addEventListener("scroll", (e) => {
            console.log("scrolling >> scroll the element");
            scrolling = true;
        }, {capture: true, passive: false})
        parentDiv?.addEventListener("wheel", (e) => {
          if(scrolling) return scrolling = false;
            console.log("not scrolling >> go to next page");
        }, {passive: true});
    }, [])

    console.log("loading layout");
    const loadView = () => {
        if (error) return <ErrorPage />;
        if (pathname === "/dashboard") {
            if(isLoading) return <Box display="flex" justifyContent="center" width="100%" height="100%" alignItems="center"><CircularProgress /></Box>
            if(user?.username) return <DashboardPage />
            return <ErrorPage />
        }
        return <Outlet />;
    }

    return (
        <ThemeProvider theme={darkTheme}>
            <CssBaseline />
            <div ref={parent} id="wheeler" style={{width:"100vw", height: "100vh", overflowY: "auto"}}>
                <NavBar />
                <Stack direction="row" sx={{width: "100vw", height: PAGE_HEIGHT}}>
                <Box sx={{display: {sm: "flex", xs: "none"}}}>
                    <SocialMedia />
                </Box>
                {loadView()}
                </Stack>
            </div>
        </ThemeProvider>
    );
}

export default Layout;

What I tried is, I listened for both the events, i.e. ‘scroll capture’ and ‘wheel’ because scroll event does not trigger when nothing is scrolled so I had to listen for ‘wheel’ event in the parent div. and since ‘scroll’ event does not bubble so scroll capture was my option.

and use a variable if, the scroll has happened then set it to true and do not call the navigate function, but if the scroll event is not fired but wheel event is fired, then navigate the user.

but the problem is the wheel event seems to fire before the scroll event. and scroll event doesn’t always fire. so there is no way to know if the scroll has really happened in the wheel event listener, as the scroll event fires only after this wheel event…

I tried using time delay in the wheel event. i.e. setTimeout, and check if the scroll has happened, it still does not work.

2

Answers


  1. Chosen as BEST ANSWER

    Well, finally I got it working, for anyone who wants to do it in the future, here is how I did it:

    const pages = ["/", "/about", "/projects", "/contact"];
    let hasScrolled = false;
    let pageIndex = 0;
    
    function Layout({ error }: Props) {
        const { pathname } = useLocation();
        const navigate = useNavigate();
        const parent = useRef<HTMLDivElement>(null);
        
        const wheelStopListener = (
            element: HTMLDivElement,
            callback: (ev: WheelEvent) => void
            ) => {
                let handle: number | null = null;
                
                const onScroll = (e: WheelEvent) => {
                    if (handle) {
                        clearTimeout(handle);
                    }
                    handle = setTimeout(() => callback(e), 900);
                };
                element.addEventListener("wheel", onScroll);
            };
            
            useEffect(() => {
                 pageIndex = pages.indexOf(pathname);
    
                const parentDiv = parent.current;
                if (!parentDiv) return;
    
            window.addEventListener(
                "scrollend",
                () => {
                    hasScrolled = true;
                },
                { capture: true }
            );
    
            wheelStopListener(parentDiv, (e: WheelEvent) => {
                if(hasScrolled) return hasScrolled = false;
                if(!hasScrolled) {
                    if(e.deltaY < 0) {
                        // scrolling down
                        if (pageIndex === 0) return;
                        console.log("move back");
                        --pageIndex;
                        navigate(pages[pageIndex]);
                    }
                    else if(e.deltaY > 0) {
                        // scrolling up
                        if(pageIndex === pages.length - 1) return;
                            ++pageIndex;
                            console.log("move front");
                            navigate(pages[pageIndex]);
                    }
                } 
            });
        }, []);
    
     return (
            <ThemeProvider theme={darkTheme}>
                <CssBaseline />
                <div
                    ref={parent}
                    id="wheeler"
                    style={{ width: "100vw", height: "100vh", overflowY: "auto" }}
                >
                    <NavBar />
                    <Stack
                        direction="row"
                        sx={{ width: "100vw", height: PAGE_HEIGHT }}
                    >
                        <Box sx={{ display: { sm: "flex", xs: "none" } }}>
                            <SocialMedia />
                        </Box>
                        <Outlet /> {/* react router outlet */} 
                    </Stack>
                </div>
            </ThemeProvider>
        );
    }
    
    export default Layout;


  2. The problem you are facing is that the wheel event fires before the scroll event. This means that if you check for the scroll event in the wheel event handler, it will always be false.

    To solve this problem, you can use the throttle() function from the lodash library. The throttle() function takes a function and a delay as arguments, and it returns a new function that will only be called when the original function is called again after the delay has passed.

    Here is the code that you can use:

    import { throttle } from 'lodash';
    
    const isScrolling = throttle(() => {
      // Check if the element is scrolling
    }, 500);
    
    const parentDiv = parent.current;
    parentDiv?.addEventListener("wheel", (e) => {
      // If the element is not scrolling, navigate to the next page
      if (!isScrolling()) {
        navigate('/next-page');
      }
    });
    

    The throttle() function will only call the isScrolling() function every 500 milliseconds. This means that if the user scrolls the element, the isScrolling() function will only be called once, after the 500 milliseconds have passed.

    const myFunction = () => {
      // action after 500ms
      setTimeout(() => {
        // ...
      }, 500);
    };
    

    To wait for 500 ms, you can also use the setTimeout() function. there are ways to detect scrolling in React and JavaScript without using a third-party library. One way is to use the window.onscroll event listener. For example:

    window.addEventListener("scroll", () => {
      // scroll action 
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search