Create a React Context Menu That Closes When Clicking Outside

Matan Kastel
3 min readMay 25, 2020

The setup

The first thing we would like to implement is a custom hook called useOutside, which handles the binding of the mouse click event. In this hook we would implement the logic to find out if the click has occurred outside the required reference or within the requested area thus opening the menu.

import { useEffect } from 'react';

function useOutside(containerRef, contextMenuRef, callback = () => {}) {
/**
* if clicked on outside of element
*/
function handleClickOutside(event) {
if (containerRef?.current?.contains(event.target) &&
!contextMenuRef?.current?.contains(event.target)
) {
callback();
}
}

useEffect(() => {
// Bind the event listener
document.addEventListener('mousedown', handleClickOutside);

return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside);
};
});
}

export default useOutside;

Now that we have this useOutside hook, we can move to the next part of creating a OutsideContext. That context will be used later to inject the container reference.

import { createContext } from 'react';

const OutsideContext = createContext(null);

export default OutsideContext;

The next step is the actual Context Menu React component that uses the useOutside & OutsideContext we’ve created before. We will be using some styled components to make the required styling but you can use any CSS you’d like. Our ContextMenu Component has some props. The left and top are used to position the menu, the ContextMenu component renders the children which allows to dynamically set the children.

import React, { useContext, useRef } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

import { OutsideContext } from 'app/contexts';
import { useOutside } from 'app/hooks';

const ContextMenuContainer = styled.div`
position: absolute;
z-index: 1;

${(props) => `
top: ${props.top || 0}px;
left: ${props.left || 0}px;
`}
`;

function ContextMenu({
children, top, left, close,
}) {
const outerRef = useContext(OutsideContext);
const contextMenuRef = useRef(null);

useOutside(outerRef, contextMenuRef, close);

return (
<ContextMenuContainer
ref={ contextMenuRef }
top={ top }
left={ left }
>
{children}
</ContextMenuContainer>
);
}

ContextMenu.propTypes = {
children: PropTypes.node,
top: PropTypes.number,
left: PropTypes.number,
close: PropTypes.func,
};

ContextMenu.defaultProps = {
children: null,
top: 0,
left: 0,
close: () => {},
};

export default ContextMenu;

We now need to handle setting up the context provider so we could have the outerRef constant set. We can do so by creating a div in the top level. The following div allows us to compare the refs with the contain function used before at useOutside.

This is used in the root App component bu of course can be implemented anywhere in the application.

import React, { useRef } from 'react';
import MyComponent from 'app/components/my-component';

function App() {
const containerRef = useRef(null);

return (
<div ref={ containerRef }>
<OutsideContext.Provider value={ containerRef }>
<MyComponent />
</<OutsideContext.Provider>
</div>
);
}

We are almost there, hang in there only a small phase to go.

Making it work

Now, we just need to render the ContextMenu component. We’ll set up some useState hooks for the component state along some functions that will implement the logic we want.

import React, { useState, useCallback } from 'react';

export default function MyComponent() {
const [contextMenuProperties, setContextMenuProperties] = useState({
contextMenuVisibility: false,
contextMenuTop: 0,
contextMenuLeft: 0,
});

const onClose = useCallback(() => {
setContextMenuProperties({
contextMenuVisibility: false,
contextMenuTop: 0,
contextMenuLeft: 0,
});
}, [setContextMenuProperties]);

const onRightClick = useCallback(({ event, selection }) => {
setContextMenuProperties({
contextMenuVisibility: true,
contextMenuTop: event.clientY,
contextMenuLeft: event.clientX,
});
}, [setContextMenuProperties]);

return (
<div>
<span> some content </span>
{
contextMenuVisibility && (
<ContextMenuContainer
top={ contextMenuTop }
left={ contextMenuLeft }
close={ onClose }
>
<div onClick={ onClose }> Item 1 </div>
<div onClick={ onClose }> Item 2 </div>
</ContextMenuContainer>
)
}
<div onContextMenu={onRightClick}> some more content that displays the menu </div>
</div>
);
}

That’s it! We now have a dynamic content menu component that is reusable across the application.

The code source for the examples above can be found here, along side a running demo here.

Thanks for reading!

--

--