javascript - Uber 吃带滚动箭头的 Horizontal ScrollSpy
问题描述
如果菜单类别大于可用的总宽度并且当用户向下滚动时,菜单活动链接会根据正在查看的当前类别不断变化,我正在寻找的是带有自动水平滚动的超级菜单样式。
我目前正在使用 material-ui,Appbar, Tabs and TabPanel
它只允许同时显示单个类别项目,而不是全部,我必须单击每个类别才能查看该类别项目,不像 uber eats 你可以继续滚动向下和顶部菜单类别指示器不断反映当前位置。我搜索了很多,但我没有找到任何解决我的问题的方法,甚至没有找到远程相关的解决方案。任何帮助、建议或指南将不胜感激,或者如果有任何与此相关的指南我错过了,链接到那将是很棒的。
解决方案
通过遵循此代码沙箱 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(/=>|<| \/>|<code>|<\/code>|'/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;
我得到一系列类别对象,它们本身有一系列产品。经过我的定制,结果如下:
推荐阅读
- r - 重新分配数据变量时出现ggplot问题
- javascript - 在 Express Router 中动态更改模型/模式
- mysql - 如何使用查询生成器 updateOrInsert 方法?
- macos - 无法在 Mac 中将 PHP 7.1 升级到 7.2
- javascript - JS 移动菜单切换也会影响带有子菜单的子菜单
- django - 如何使用 redis 数据库在 django 中实现一致性哈希?
- c++ - 如何获取使用全局变量的所有函数名称?
- javascript - 如何应用多个弹出框背景颜色
- jestjs - 从导入的文件中模拟一个函数,同时检查它是否被调用
- c++ - c ++如何使用for循环查找重复项