首页 > 解决方案 > Uber 吃带滚动箭头的 Horizo​​ntal ScrollSpy

问题描述

如果菜单类别大于可用的总宽度并且当用户向下滚动时,菜单活动链接会根据正在查看的当前类别不断变化,我正在寻找的是带有自动水平滚动的超级菜单样式。 在此处输入图像描述

我目前正在使用 material-ui,Appbar, Tabs and TabPanel它只允许同时显示单个类别项目,而不是全部,我必须单击每个类别才能查看该类别项目,不像 uber eats 你可以继续滚动向下和顶部菜单类别指示器不断反映当前位置。我搜索了很多,但我没有找到任何解决我的问题的方法,甚至没有找到远程相关的解决方案。任何帮助、建议或指南将不胜感激,或者如果有任何与此相关的指南我错过了,链接到那将是很棒的。

标签: javascriptreactjs

解决方案


通过遵循此代码沙箱 https://codesandbox.io/s/material-demo-xu80m?file=/index.js 并根据我的需要对其进行自定义,我确实想出了使用 MaterialUI 所需的滚动效果。

自定义组件代码为:

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;


const { tabsInScroll } = props;我得到一系列类别对象,它们本身有一系列产品。经过我的定制,结果如下: 在此处输入图像描述


推荐阅读