mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-02-02 13:17:57 +01:00
chore(ContextMenu): new component
This commit is contained in:
@@ -165,6 +165,20 @@
|
|||||||
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||||
|
Context menu:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UCard ref="contextMenuRef" class="relative" body-class="h-64" @click="isContextMenuOpen = false" @contextmenu.prevent="isContextMenuOpen = true">
|
||||||
|
<UContextMenu v-model="isContextMenuOpen" :virtual-element="virtualElement" width-class="w-48">
|
||||||
|
<UCard @click.stop>
|
||||||
|
Menu
|
||||||
|
</UCard>
|
||||||
|
</UContextMenu>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
<div class="font-medium text-sm mb-1 u-text-gray-700">
|
||||||
Command palette:
|
Command palette:
|
||||||
@@ -266,6 +280,32 @@ const form = reactive({
|
|||||||
|
|
||||||
const { $toast } = useNuxtApp()
|
const { $toast } = useNuxtApp()
|
||||||
|
|
||||||
|
const x = ref(0)
|
||||||
|
const y = ref(0)
|
||||||
|
const isContextMenuOpen = ref(false)
|
||||||
|
const contextMenuRef = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('mousemove', ({ clientX, clientY }) => {
|
||||||
|
x.value = clientX
|
||||||
|
y.value = clientY
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const virtualElement = computed(() => ({
|
||||||
|
getBoundingClientRect () {
|
||||||
|
return {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
top: y.value,
|
||||||
|
right: x.value,
|
||||||
|
bottom: y.value,
|
||||||
|
left: x.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contextElement: contextMenuRef.value?.$el
|
||||||
|
}))
|
||||||
|
|
||||||
const customQuery = query => computed(() => query.value ? `${query.value} | =1` : '')
|
const customQuery = query => computed(() => query.value ? `${query.value} | =1` : '')
|
||||||
|
|
||||||
function toggleModalIsOpen () {
|
function toggleModalIsOpen () {
|
||||||
|
|||||||
73
src/runtime/components/overlays/ContextMenu.vue
Normal file
73
src/runtime/components/overlays/ContextMenu.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="isOpen" ref="container" :class="[containerClass, widthClass]">
|
||||||
|
<transition appear v-bind="transitionClass">
|
||||||
|
<div :class="baseClass">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType, computed, toRef } from 'vue'
|
||||||
|
import { defu } from 'defu'
|
||||||
|
import { usePopper } from '../../composables/usePopper'
|
||||||
|
import type { PopperOptions } from './../types'
|
||||||
|
import $ui from '#build/ui'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
virtualElement: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
wrapperClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.wrapper
|
||||||
|
},
|
||||||
|
containerClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.container
|
||||||
|
},
|
||||||
|
widthClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.width
|
||||||
|
},
|
||||||
|
baseClass: {
|
||||||
|
type: String,
|
||||||
|
default: () => $ui.contextMenu.base
|
||||||
|
},
|
||||||
|
transitionClass: {
|
||||||
|
type: Object,
|
||||||
|
default: () => $ui.contextMenu.transition
|
||||||
|
},
|
||||||
|
popperOptions: {
|
||||||
|
type: Object as PropType<Pick<PopperOptions, 'strategy' | 'placement'>>,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'close'])
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get () {
|
||||||
|
return props.modelValue
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const virtualElement = toRef(props, 'virtualElement')
|
||||||
|
|
||||||
|
const popperOptions = computed(() => defu({}, props.popperOptions, { placement: 'bottom-start' }))
|
||||||
|
|
||||||
|
const [, container] = usePopper(popperOptions.value, virtualElement)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'UContextMenu' }
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { ref, onMounted, watchEffect } from 'vue'
|
import { ref, onMounted, watchEffect } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
import { popperGenerator, defaultModifiers } from '@popperjs/core/lib/popper-lite'
|
import { popperGenerator, defaultModifiers } from '@popperjs/core/lib/popper-lite'
|
||||||
|
import type { Instance } from '@popperjs/core'
|
||||||
import { omitBy, isUndefined } from 'lodash-es'
|
import { omitBy, isUndefined } from 'lodash-es'
|
||||||
import flip from '@popperjs/core/lib/modifiers/flip'
|
import flip from '@popperjs/core/lib/modifiers/flip'
|
||||||
import offset from '@popperjs/core/lib/modifiers/offset'
|
import offset from '@popperjs/core/lib/modifiers/offset'
|
||||||
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow'
|
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow'
|
||||||
|
|
||||||
const createPopper = popperGenerator({
|
export const createPopper = popperGenerator({
|
||||||
defaultModifiers: [...defaultModifiers, offset, flip, preventOverflow]
|
defaultModifiers: [...defaultModifiers, offset, flip, preventOverflow]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -16,22 +18,23 @@ export function usePopper ({
|
|||||||
offsetSkid = 0,
|
offsetSkid = 0,
|
||||||
placement,
|
placement,
|
||||||
strategy
|
strategy
|
||||||
}) {
|
}, virtualReference) {
|
||||||
const reference = ref(null)
|
const reference: Ref<HTMLElement> = ref(null)
|
||||||
const popper = ref(null)
|
const popper: Ref<HTMLElement> = ref(null)
|
||||||
|
const instance: Ref<Instance> = ref(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
watchEffect((onInvalidate) => {
|
watchEffect((onInvalidate) => {
|
||||||
if (!popper.value) { return }
|
if (!popper.value) { return }
|
||||||
if (!reference.value) { return }
|
if (!reference.value && !virtualReference.value) { return }
|
||||||
|
|
||||||
const popperEl = popper.value.$el || popper.value
|
const popperEl = popper.value.$el || popper.value
|
||||||
const referenceEl = reference.value.$el || reference.value
|
const referenceEl = virtualReference?.value || reference.value.$el || reference.value
|
||||||
|
|
||||||
if (!(referenceEl instanceof HTMLElement)) { return }
|
// if (!(referenceEl instanceof HTMLElement)) { return }
|
||||||
if (!(popperEl instanceof HTMLElement)) { return }
|
if (!(popperEl instanceof HTMLElement)) { return }
|
||||||
|
|
||||||
const { destroy } = createPopper(referenceEl, popperEl, omitBy({
|
instance.value = createPopper(referenceEl, popperEl, omitBy({
|
||||||
placement,
|
placement,
|
||||||
strategy,
|
strategy,
|
||||||
modifiers: [{
|
modifiers: [{
|
||||||
@@ -50,9 +53,9 @@ export function usePopper ({
|
|||||||
}]
|
}]
|
||||||
}, isUndefined))
|
}, isUndefined))
|
||||||
|
|
||||||
onInvalidate(destroy)
|
onInvalidate(instance.value.destroy)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return [reference, popper]
|
return [reference, popper, instance]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -503,6 +503,21 @@ export default (variantColors: string[]) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contextMenu = {
|
||||||
|
wrapper: 'relative',
|
||||||
|
container: 'z-20',
|
||||||
|
width: '',
|
||||||
|
base: '',
|
||||||
|
transition: {
|
||||||
|
enterActiveClass: 'transition ease-out duration-200',
|
||||||
|
enterFromClass: 'opacity-0 translate-y-1',
|
||||||
|
enterToClass: 'opacity-100 translate-y-0',
|
||||||
|
leaveActiveClass: 'transition ease-in duration-150',
|
||||||
|
leaveFromClass: 'opacity-100 translate-y-0',
|
||||||
|
leaveToClass: 'opacity-0 translate-y-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
card,
|
card,
|
||||||
modal,
|
modal,
|
||||||
@@ -527,6 +542,7 @@ export default (variantColors: string[]) => {
|
|||||||
slideover,
|
slideover,
|
||||||
notification,
|
notification,
|
||||||
tooltip,
|
tooltip,
|
||||||
popover
|
popover,
|
||||||
|
contextMenu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user