Implementing custom context menu in react.js

Tarak
5 min readMay 1, 2019

--

By the end of this article, we would write the code to implement the following

custom context menu in react js

In this article, we will look at how we can override the default browser’s context menu and replace it with our own in react.js.

Since we are talking about context menu, first we have to figure out how to capture the context menu / right click event in javascript. Then, we have to disable the browser default one with our own menu.

In order to capture the right click event from the browser, we have to listen to the ‘contextmenu’ event from the browser window. Once the event is captured, disable the default action performed. Javascript code for the same can be written as follows

document.addEventListener(‘contextmenu’, function(event){
event.preventDefault();})

Now, we have to capture the x,y coordinates and add our own menu at that particular position on the page.

Now, let us start writing our component. We will be creating a component called ‘CustomContext’ and pass menu items as ‘items’ property. For this example, I will be passing the following json as the list of items to be displayed in the context menu. We store these values in the state.

menu = [{“label”:”Item 1"},{“label”:”Menu item 2"},{“label”:”Apple”},{“label”:”This is orange”},{“label”:”Conetxt menu is fun”},{“label”:”Cool”}]

We can add the context menu to the page using the following code:

<CustomContext items={this.state.menu}>
</CustomContext>

Let us start writing the CustomContext component. We will be adding the eventListener in the componentDidMount(), write logic to convert the list to a menu and update the x,y position based on mouse click. Also, if a left click is done, we will hide the menu.

Code for the same will be as follows:

class CustomContext extends React.Component{
constructor(props) {
super(props);

this.state={
visible: false,
x: 0,
y: 0
};
}

componentDidMount(){
var self=this;
document.addEventListener(‘contextmenu’, function(event){
event.preventDefault();
const clickX = event.clientX;
const clickY = event.clientY;
self.setState({ visible: true, x: clickX, y: clickY });

});
document.addEventListener(‘click’, function(event){
event.preventDefault();
self.setState({ visible: false, x:0, y:0});

});
}

returnMenu(items){
var myStyle = {
‘position’: ‘absolute’,
‘top’: `${this.state.y}px`,
‘left’:`${this.state.x+5}px`
}


return <div className=’custom-context’ id=’text’ style={myStyle}>
{items.map((item, index, arr) =>{
if(arr.length-1==index){
return <div key={index} className=’custom-context-item-last’>{item.label}</div>
}else{
return <div key={index} className=’custom-context-item’>{item.label}</div>
}
})}
</div>;
}

In the componentDidMount function, we have added event listener for ‘contextmenu’ which gets called when right click is done on the page. Inside the function, we have stopped the default action using event.preventDefault() and set the visibility to true and updated the x,y coordinates to the clicked position. Also, for the click event listener, we are setting x,y values to 0,0 and set the visibility to false so that our context menu would be hidden.

Next is the returnMenu() function that accepts the list of items as the parameter. We wrap the items in an outer div with id ‘text’, iterate the list using map function and add as a childern. Also, the style of the outer div has been updated such that it would be placed at the x,y position that has been already set.

Little css styling has been done such that the menu items appear with dotted bottom border (except the last one) and surrounded by a box with some padding.

Here is the css for the same:

.custom-context{
border: solid 1px #ccc;
display: inline-block;
margin: 5px;
background: #FFF;
color: #000;
font-family: sans-serif;
cursor: pointer;
font-size: 12px;
}
.custom-context-item{
border-bottom: dotted 1px #ccc;
padding: 5px 25px;
}
.custom-context-item-last{
padding: 5px 25px;
}

Now, the last part, a condition has to be written in the render block such that if the visibility value is true, we display the div/context menu created.

render() {
return (<div id=’cmenu’>
{this.state.visible ? this.returnMenu(this.props.items): null}
</div>
)
}

Now that our context menu has been in place, we will look at how we can perform some action on clicking on the menu item.

In order to add the callbacks for the context menu items, let’s add a callback property to the items list in the state and also add the corresponding functions as follows in the main class in which the CustomContext component has been loaded. Here, we just added alert boxes to check if the functionality works

this.state={
“menu”:[
{“label”: “Item 1”, “callback”: this.itemCallback},
{“label”: “Menu item 2”, “callback”: this.item2Callback},
{“label”: “Apple”, “callback”: this.appleCallback},
{“label”: “This is orange”, “callback”: this.orangeCallback},
{“label”: “Conetxt menu is fun”, },
{“label”: “Cool”, “callback”: this.coolCallback}
]
}
}

itemCallback() {
alert(“clicked on Item 1”)
}

item2Callback() {
alert(“clicked on Item 2”)
}

appleCallback() {
alert(“clicked on Apple”)
}
orangeCallback() {
alert(“clicked on Orange”)
}
coolCallback(){
alert(“clicked on Cool”)
}

Update the return statement of returnMenu() function with ref and index as follows:

return <div className=’custom-context’ id=’customcontext’ style={myStyle} ref={this.contextRef}>
{items.map((item, index, arr) =>{

if(arr.length-1==index){
return <div key={index} className=’custom-context-item-last’ index={index}>{item.label}</div>
}else{
return <div key={index} className=’custom-context-item’ index={index}>{item.label}</div>
}
})}
</div>;

As we could see, a reference has been added to the outer container of the context menu and a new attribute, index has been added which would be used in the function where click event has been captured.

Now, let us update the code of the click event listener as follows:

document.addEventListener(‘click’, function(event){

if(self.contextRef.current.id==’customcontext’){
self.click(event.target.getAttribute(‘index’));
}

event.preventDefault();
self.setState({ visible: false, x:0, y:0});

});

Here, we are checking if the id of the container clicked equals ‘customcontext’ which is the id of the outer container of our context menu. If the condition matches, we get the index of the clicked div and pass the value to another function called click.

Inside the click function, we can invoke the callback function that has been passed to the child using props.item.

Our click function would be as follows (with a little check added for checking if callback is present for the menu item):

click(index) {
if(this.props.items[index].callback)
this.props.items[index].callback();
else{
console.log(“callback not registered for the menu item”)
}
}

Our custom component is now ready with our own actions. We could define the functionality of the context menu item in parent component and use the ‘callback’ key to pass the values to the child as a property.

Working example of the same can be found in the following plunker

Next Steps: I have done the basic context menu which contains only one level. Code can be updated to support sub-menus as well where we should be able to provide the data using nested json and update the returnMenu() function to display the nested items which should be easier to implement on top of this.

Happy Learning :-)

Thank you!

--

--