chore(ContextMenu): new component

This commit is contained in:
Benjamin Canac
2022-10-08 00:51:53 +02:00
parent 3ed64250cd
commit b9f0b3cb10
4 changed files with 143 additions and 11 deletions

View File

@@ -165,6 +165,20 @@
<UButton icon="heroicons-outline:bell" variant="red" label="Trigger an error" @click="onNotificationClick" />
</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 class="font-medium text-sm mb-1 u-text-gray-700">
Command palette:
@@ -266,6 +280,32 @@ const form = reactive({
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` : '')
function toggleModalIsOpen () {

View 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>

View File

@@ -1,11 +1,13 @@
import { ref, onMounted, watchEffect } from 'vue'
import type { Ref } from 'vue'
import { popperGenerator, defaultModifiers } from '@popperjs/core/lib/popper-lite'
import type { Instance } from '@popperjs/core'
import { omitBy, isUndefined } from 'lodash-es'
import flip from '@popperjs/core/lib/modifiers/flip'
import offset from '@popperjs/core/lib/modifiers/offset'
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow'
const createPopper = popperGenerator({
export const createPopper = popperGenerator({
defaultModifiers: [...defaultModifiers, offset, flip, preventOverflow]
})
@@ -16,22 +18,23 @@ export function usePopper ({
offsetSkid = 0,
placement,
strategy
}) {
const reference = ref(null)
const popper = ref(null)
}, virtualReference) {
const reference: Ref<HTMLElement> = ref(null)
const popper: Ref<HTMLElement> = ref(null)
const instance: Ref<Instance> = ref(null)
onMounted(() => {
watchEffect((onInvalidate) => {
if (!popper.value) { return }
if (!reference.value) { return }
if (!reference.value && !virtualReference.value) { return }
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 }
const { destroy } = createPopper(referenceEl, popperEl, omitBy({
instance.value = createPopper(referenceEl, popperEl, omitBy({
placement,
strategy,
modifiers: [{
@@ -50,9 +53,9 @@ export function usePopper ({
}]
}, isUndefined))
onInvalidate(destroy)
onInvalidate(instance.value.destroy)
})
})
return [reference, popper]
return [reference, popper, instance]
}

View File

@@ -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 {
card,
modal,
@@ -527,6 +542,7 @@ export default (variantColors: string[]) => {
slideover,
notification,
tooltip,
popover
popover,
contextMenu
}
}