72
loading...
This website collects cookies to deliver better user experience
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport. — developer.mozilla.org
We need to know all elements that are present inside the container.
We also need information of which one’s are visible and which one’s are overflowing the container on resizing.
Once we have both the information we can just control the visibility of elements with CSS and render overflowing items inside the overflow menu.
import React from "react";
import Button from "@material-ui/core/Button";
import { makeStyles } from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete";
import CloudUploadIcon from "@material-ui/icons/CloudUpload";
import KeyboardVoiceIcon from "@material-ui/icons/KeyboardVoice";
import Icon from "@material-ui/core/Icon";
import ArchiveIcon from "@material-ui/icons/Archive";
import StarIcon from "@material-ui/icons/Star";
import SaveIcon from "@material-ui/icons/Save";
import DownloadIcon from "@material-ui/icons/ArrowDownward";
import IntersectionObserverWrapper from "./intersection-observer-wrapper";
const useStyles = makeStyles((theme) => ({
button: {
margin: theme.spacing(1),
display: "flex",
flex: "0 0 auto"
}
}));
export default function IconLabelButtons() {
const classes = useStyles();
return (
<IntersectionObserverWrapper>
<Button
color="primary"
data-targetid="save"
className={classes.button}
startIcon={<SaveIcon />}
>
Save
</Button>
<Button
color="primary"
data-targetid="upload"
className={classes.button}
startIcon={<CloudUploadIcon />}
>
Upload
</Button>
<Button
color="primary"
data-targetid="download"
className={classes.button}
startIcon={<DownloadIcon />}
>
Download
</Button>
<Button
color="primary"
data-targetid="send"
className={classes.button}
startIcon={<Icon>send</Icon>}
>
Send
</Button>
<Button
color="primary"
data-targetid="star"
className={classes.button}
startIcon={<StarIcon />}
>
Star
</Button>
<Button
color="primary"
data-targetid="archive"
className={classes.button}
startIcon={<ArchiveIcon />}
>
Archive
</Button>
<Button
color="primary"
data-targetid="delete"
className={classes.button}
startIcon={<DeleteIcon />}
>
Delete
</Button>
</IntersectionObserverWrapper>
);
}
Create a new instance of IntersectionObserver.
Pass root element as the container of the child elements. (navRef.current in our example)
Set the threshold to 1. This means that, at any change in 100% of the target visibility our Observers callback will be executed.
In the observer callback we will update a state map to track visibility of elements.
After creating the Observer, we need to observe our targets i.e. menu items. For this, we get all the children of ancestor node which in our case is referenced with navRef and add as target only if it has a data-targetid property.
Both the logic of creating observer and adding targets can be added inside useEffect which runs post initial render.
Remember to disconnect the observer on unmount of the component.
export default function IntersectionObserverWrapper({ children }) {
const classes = useIntersectionStyles();
const navRef = useRef(null);
const [visibilityMap, setVisibilityMap] = useState({});
const handleIntesection = () => {
// our logic of maintaining visibility state goes here
}
useEffect(() => {
const observer = new IntersectionObserver(
handleIntersection,
{
root: navRef.current,
threshold: 1
}
);
// We are addting observers to child elements of the container div
// with ref as navRef. Notice that we are adding observers
// only if we have the data attribute targetid on the child element
Array.from(navRef.current.children).forEach((item) => {
if (item.dataset.targetid) {
observer.observe(item);
}
});
return () = {
observer.disconnect();
}
}, []);
return (
<div className={classes.toolbarWrapper} ref={navRef}>
{children}
</div>
);
}
const handleIntersection = (entries) => {
const updatedEntries = {};
entries.forEach((entry) => {
const targetid = entry.target.dataset.targetid;
// Check if element is visibile within container
if (entry.isIntersecting) {
updatedEntries[targetid] = true;
} else {
updatedEntries[targetid] = false;
}
});
// Overwrite previous state values with current state
setVisibilityMap((prev) => ({
...prev,
...updatedEntries
}));
};
return (
<div className={classes.toolbarWrapper} ref={navRef}>
{React.Children.map(children, (child) => {
return React.cloneElement(child, {
className: classnames(child.props.className, {
[classes.visible]: !!visibilityMap[child.props["data-targetid"]],
[classes.inVisible]: !visibilityMap[child.props["data-targetid"]]
})
});
})}
</div>
);
return (
<div className={classes.toolbarWrapper} ref={navRef}>
{React.Children.map(children, (child) => {
return React.cloneElement(child, {
className: classnames(child.props.className, {
[classes.visible]: !!visibilityMap[child.props["data-targetid"]],
[classes.inVisible]: !visibilityMap[child.props["data-targetid"]]
})
});
})}
<OverflowMenu
visibilityMap={visibilityMap}
className={classes.overflowStyle}
>
{children}
</OverflowMenu>
</div>
);
<Menu
id="long-menu"
anchorEl={anchorEl}
keepMounted
open={open}
onClose={handleClose}
>
{React.Children.map(children, (child) => {
if (!props.visibilityMap[child.props["data-targetid"]]) {
return (
<MenuItem key={child} onClick={handleClose}>
{React.cloneElement(child, {
className: classnames(child.className, classes.inOverflowMenu)
})}
</MenuItem>
);
}
return null;
})}
</Menu>
const useIntersectionStyles = makeStyles(() => ({
visible: {
order: 0,
opacity: 1
},
inVisible: {
order: 100,
opacity: 0,
pointerEvents: "none"
},
toolbarWrapper: {
display: "flex",
overflow: "hidden",
padding: "0 20px"
},
overflowStyle: {
order: 99,
position: "sticky",
right: "0",
backgroundColor: "white"
}
}));