SoFunction
Updated on 2025-04-07

Detailed explanation of the example of Vue3 development right-click menu

Preface

Since I personally do many projects in the background management, the right-click menu is also a relatively high-frequency component. However, the technology stack I personally use is Vue3. At present, the community does not have a good plug-in to use, so I can only choose to build my own wheels.

Basic content of directory structure

In the initial stage, I divided the menu component into two directories and named itand,The two components each perform their duties,Components provide outermost container positioning and hierarchical capabilities,Provides style and current time event callbacks for each item.

ContextMenu component, I expect to insert it in the body, this is to facilitate component positioning (position), then at this time you can use the help of Vue3TeleportComponents achieve this effect. Since my business scenario is right-clicking in the table, if I go to each row (tr) or each cell (td) All generate a menu component, which will result in multiple menu components in the body. This didn't fit my expectations, so I decided to usev-ifTo control the display and hiding of components. The basic HTML structure is as follows:

 <Teleport to="body" v-if="visible">
      <div
        class="contextMenu"
        ref="contextmenuRef"
      >
      </div>
  </Teleport>
  <script lang="ts" setup>
      const visible = ref(false)
  </script>
  <style>
      .contextMenu {
          position: absolute;
          min-width: 150px;
          min-height:100px;
          padding-top: 5px;
          padding-bottom: 8px;
          background-color: #fff;
          border-radius: 4px;
        }
  </style>

Calculate the position of the ContextMenu component (position)

Want to knowContextMenuWhere will the component appear? We need to know how it is used in the component? I'm assuming there is a .vue component

&lt;el-button @contextmenu="contextmenuFun"&gt;Button&lt;/el-button&gt;
    &lt;Contextmenu ref="ContextMenuRef"&gt;
      
    &lt;/Contextmenu&gt;
import { ref } from 'vue'
const ContextMenuRef = ref()
const contextmenuFun = (e) =&gt; {
  (e)
}

On the business side, you can see it. I expect a trigger point, whether it is a button or HTML element. This trigger point requires manual call to the show method in the ContextMenu component, and the current trigger event source needs to beevent) Passed over. Then let's go backContextMenuIt's easy to write in the componentshowThe logic of the method.

const position = ref({
  top: 0,
  left: 0
})
const style = computed(() => {
  return {
    left: ,
    top: 
  }
})

const show = (e: MouseEvent) => {
  (e, "e")
  ()
   = true
}

Then the location where the contextMenu appears needs to be calculated dynamically. Note that the point is the location where the occurrence is, and we need to calculate the boundary value.

...
// Calculate the offset value of x and yconst calculatePosition = (axis: "X" | "Y", mousePos: number, elSize: number) =&gt; {
  const windowSize = axis === "X" ?  : 
  const scrollPos = axis === "X" ?  : 

  let pos = mousePos - scrollPos
  if (pos + elSize &gt; windowSize) {
    pos = (0, pos - elSize)
  }

  return pos + scrollPos
}

const show = async (e: MouseEvent) =&gt; {
  ()
   = true
  await nextTick()
  const el = 
  if (!el) {
    return
  }
  const width = 
  const height = 
  const { pageX: x, pageY: y } = e
   = calculatePosition("Y", y, height)
   = calculatePosition("X", x, width)
  (, "w")
}
...

We passedcalculatePositionCalculate the valid x,y, and use itMake sure the display does not exceed the current screen.

Click outside the menu to hide

How to tell if you click outside the menu to hide it? At this time, you need to use the help of the click objecteventThe event is processed, and the peripheral of the processing click element is used as a hook and nameduseClickOutside

import { onMounted, onBeforeUnmount, Ref } from "vue"

function useClickOutside(elementRef: Ref<HTMLElement | null>, callback: (event: MouseEvent) => void): void {
  const clickOutsideHandler = (event: MouseEvent) => {
    const el = 
    if (!el || el ===  || ().includes(el)) {
      return
    }
    callback(event)
  }

  onMounted(() => {
    ("click", clickOutsideHandler)
  })

  onBeforeUnmount(() => {
    ("click", clickOutsideHandler)
  })
}

export default useClickOutside
 <div class="contextMenu" ref="contextmenuRef" :style="style">1234</div>
const contextmenuRef = ref<HTMLDivElement | null>(null)
import useClickOutside from "./UseClickOutSide"
useClickOutside(contextmenuRef, () => {
   = false
})

At this time, we can click outside the menu to hide the menu, but there will be another problem, that is, if, right-click to expand the menu, when I click a button, I don’t want the menu to be hidden, but I hope it will be displayed all the time. At this time, it needs to be targeteduseClickOutsideAdd an extra parameter to control. The menu is not hidden when clicking on an element

In business code, it can be passedignoreExclude HTML elements

div class="contextMenua" @contextmenu="contextmenu"&gt;123&lt;/div&gt;
  &lt;button class="ingoreBtn"&gt;Not hidden buttons&lt;/button&gt;
  &lt;ContextMenu ref="contextmenuRef" :ignore="ignore" /&gt;

Defining props in contextmenu

interface Props {
  ignore: string[]
}
const props = withDefaults(defineProps<Props>(), {
  ignore: () => [] as string[]
})
...
useClickOutside(
  contextmenuRef,
  () => {
    ("w")
     = false
  },
  { ignore:  }
)
...

Added new in useClickOutside functionIgnoreElementMethods are used to exclude HTML elements

let isIgnore = true
const IgnoreElement = (ignore: string[], event: MouseEvent) => {
    return ((target) => {
      if (typeof target === "string") {
        return ((target)).some(
          (el) => el ===  || ().includes(el)
        )
      }
    })
  }
  const clickOutsideHandler = (event: MouseEvent) => {
      ...
       if (options?.ignore &&  > 0) {
      isIgnore = !IgnoreElement(, event)
    }
    if (!isIgnore) {
      isIgnore = true
      return
    }
    ...
  
  }

We passedisIgnoreThe variables are marked to determine whether they have experienced it.IgnoreElementThe default call to true will not affect the existing logic. whenisIgnoreWhen false, we need to turn it into true to prevent the next click from being hidden.

The menu does not scroll with the scrollbar

When our page height exceeds the screen height, a scroll bar will appear. When we right-click a menu for an element, it will appear, and then scroll, we will find that our menu will also move. To solve this situation, a transparent cover can be used to cover the body, causing the original scrolling behavior to be invalidated. In this theory, the HTML structure needs to be adjusted

 <div class="contextMenu-wrapper" :class="{ 'is-fixed': fixed }">
      <div class="contextMenu" ref="contextmenuRef" :style="style" :class="[popperClass]">1234</div>
    </div>
    interface Props {
      ignore: string[]
      popperClass?: string
      isFixed: boolean
    }
    watch(
      () => ,
      () => {
        if () {
           = "hidden"
        } else {
           = 
        }
      }
    )
    const show = async (e: MouseEvent) => {
        ...
         = 
        ...
    })
    useClickOutside(
      contextmenuRef,
      () => {
         = false
         = false
      },
      { ignore:  }
    )
  onMounted(async () => {
  if () {
        await nextTick()
         = 
        const style = ()
         = 
      }
})  
<style>
.contextMenu-wrapper {
  z-index: 9999;
  background-color: transparent;
  &.is-fixed {
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }
}
</style>

Addedis-fixedVariables are used as identification of whether the masking layer is required. passwatchListen to the fixed changes, if true, body'soverflowIf it becomes hidden, if it is closed, restore the default valuedefaultSyleOverFlow

So far, the basic functions of the drop-down menu have been completed, but the following functions have not been completed yet:

  • Respond to keyboard events
  • Control of hierarchical zIndex
  • Multi-level menu (subItem)

This is the end of this article about the example of Vue3 development right-click menu. For more related contents of Vue3 right-click menu, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!