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" /> <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 () {

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 { 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]
} }

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