Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
192 views
in Technique[技术] by (71.8m points)

javascript - Uber eats type Horizontal ScrollSpy with scroll arrows

What i am looking for is uber eats type menu style with auto horizontal scroll if the menu categories are more then the total width that is available and When the user scroll down, the menu active links keeps changing according to the current category that being viewed. enter image description here

I am using material-ui at the moment and its Appbar, Tabs and TabPanel only allow a single category items to be displayed at the same time, not all, i have to click on each category to view that category items, unlike uber eats where you can just keep scrolling down and the top menu categories indicator keeps on reflecting the current position. I searched a lot but i didn't find any solution to my problem or even remotely related one too. Any help, suggestion or guide will be appreciated or if there is any guide of something related to this that i have missed, link to that will be awesome.

question from:https://stackoverflow.com/questions/65541217/uber-eats-type-horizontal-scrollspy-with-scroll-arrows

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

By following this Code Sandbox https://codesandbox.io/s/material-demo-xu80m?file=/index.js and customizing it to my needs i did came up with my required scrolling effect by using MaterialUI.

The customized component code is:

import React from "react";
import throttle from "lodash/throttle";
import { makeStyles, withStyles } from "@material-ui/core/styles";
import useStyles2 from "../styles/storeDetails";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { Grid } from "@material-ui/core";
import MenuCard from "./MenuCard";

const tabHeight = 69;
const StyledTabs = withStyles({
    root: {
        textAlign: "left !important",
    },
    indicator: {
        display: "flex",
        justifyContent: "center",
        backgroundColor: "transparent",
        "& > div": {
            maxWidth: 90,
            width: "100%",
            backgroundColor: "rgb(69, 190, 226)",
        },
    },
})((props) => <Tabs {...props} TabIndicatorProps={{ children: <div /> }} />);

const StyledTab = withStyles((theme) => ({
    root: {
        textTransform: "none",
        height: tabHeight,
        textAlign: "left !important",
        marginLeft: -30,
        marginRight: 10,
        fontWeight: theme.typography.fontWeightRegular,
        fontSize: theme.typography.pxToRem(15),
        [theme.breakpoints.down("sm")]: {
            fontSize: theme.typography.pxToRem(13),
            marginLeft: -10,
        },
        "&:focus": {
            opacity: 1,
        },
    },
}))((props) => <Tab disableRipple {...props} />);

const useStyles = makeStyles((theme) => ({
    root: {
        flexGrow: 1,
    },
    indicator: {
        padding: theme.spacing(1),
    },
    demo2: {
        backgroundColor: "#fff",
        position: "sticky",
        top: 0,
        left: 0,
        right: 0,
        width: "100%",
    },
}));

const makeUnique = (hash, unique, i = 1) => {
    const uniqueHash = i === 1 ? hash : `${hash}-${i}`;

    if (!unique[uniqueHash]) {
        unique[uniqueHash] = true;
        return uniqueHash;
    }

    return makeUnique(hash, unique, i + 1);
};

const textToHash = (text, unique = {}) => {
    return makeUnique(
        encodeURI(
            text
                .toLowerCase()
                .replace(/=&gt;|&lt;| /&gt;|<code>|</code>|&#39;/g, "")
                // eslint-disable-next-line no-useless-escape
                .replace(/[!@#$%^&*()=_+[]{}`~;:'"|,.<>/?s]+/g, "-")
                .replace(/-+/g, "-")
                .replace(/^-|-$/g, "")
        ),
        unique
    );
};
const noop = () => {};

function useThrottledOnScroll(callback, delay) {
    const throttledCallback = React.useMemo(
        () => (callback ? throttle(callback, delay) : noop),
        [callback, delay]
    );

    React.useEffect(() => {
        if (throttledCallback === noop) return undefined;

        window.addEventListener("scroll", throttledCallback);
        return () => {
            window.removeEventListener("scroll", throttledCallback);
            throttledCallback.cancel();
        };
    }, [throttledCallback]);
}

function ScrollSpyTabs(props) {
    const [activeState, setActiveState] = React.useState(null);
    const { tabsInScroll } = props;

    let itemsServer = tabsInScroll.map((tab) => {
        const hash = textToHash(tab.name);
        return {
            icon: tab.icon || "",
            text: tab.name,
            component: tab.products,
            hash: hash,
            node: document.getElementById(hash),
        };
    });

    const itemsClientRef = React.useRef([]);
    React.useEffect(() => {
        itemsClientRef.current = itemsServer;
    }, [itemsServer]);

    const clickedRef = React.useRef(false);
    const unsetClickedRef = React.useRef(null);
    const findActiveIndex = React.useCallback(() => {
        // set default if activeState is null
        if (activeState === null) setActiveState(itemsServer[0].hash);

        // Don't set the active index based on scroll if a link was just clicked
        if (clickedRef.current) return;

        let active;
        for (let i = itemsClientRef.current.length - 1; i >= 0; i -= 1) {
            // No hash if we're near the top of the page
            if (document.documentElement.scrollTop < 0) {
                active = { hash: null };
                break;
            }

            const item = itemsClientRef.current[i];

            if (
                item.node &&
                item.node.offsetTop <
                    document.documentElement.scrollTop +
                        document.documentElement.clientHeight / 8 +
                        tabHeight
            ) {
                active = item;
                break;
            }
        }

        if (active && activeState !== active.hash) {
            setActiveState(active.hash);
        }
    }, [activeState, itemsServer]);

    // Corresponds to 10 frames at 60 Hz
    useThrottledOnScroll(itemsServer.length > 0 ? findActiveIndex : null, 166);

    const handleClick = (hash) => () => {
        // Used to disable findActiveIndex if the page scrolls due to a click
        clickedRef.current = true;
        unsetClickedRef.current = setTimeout(() => {
            clickedRef.current = false;
        }, 1000);

        if (activeState !== hash) {
            setActiveState(hash);

            if (window)
                window.scrollTo({
                    top:
                        document.getElementById(hash).getBoundingClientRect().top +
                        window.pageYOffset,
                    behavior: "smooth",
                });
        }
    };

    React.useEffect(
        () => () => {
            clearTimeout(unsetClickedRef.current);
        },
        []
    );

    const classes = useStyles();
    const classes2 = useStyles2();

    return (
        <>
            <nav className={classes2.rootCategories}>
                <StyledTabs
                    value={activeState ? activeState : itemsServer[0].hash}
                    variant="scrollable"
                    scrollButtons="on"
                >
                    {itemsServer.map((item2) => (
                        <StyledTab
                            key={item2.hash}
                            label={item2.text}
                            onClick={handleClick(item2.hash)}
                            value={item2.hash}
                        />
                    ))}
                </StyledTabs>
                <div className={classes.indicator} />
            </nav>

            <div className={classes2.root}>
                {itemsServer.map((item1, ind) => (
                    <>
                        <h3 style={{ marginTop: 30 }}>{item1.text}</h3>
                        <Grid
                            container
                            spacing={3}
                            id={item1.hash}
                            key={ind}
                            className={classes2.menuRoot}
                        >
                            {item1.component.map((product, index) => (
                                <Grid item xs={12} sm={6} key={index}>
                                    <MenuCard product={product} />
                                </Grid>
                            ))}
                        </Grid>
                    </>
                ))}
            </div>
        </>
    );
}

export default ScrollSpyTabs;


In const { tabsInScroll } = props; I am getting an array of categories objects, which themselves having an array of products inside them. After my customization, this is the result: enter image description here


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...