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(/=>|<| />|<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;
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: