Components

Popover

Displays rich content in a portal, triggered by a button.

Source Code

You will need to install the HeadlessUI Float - Vue package

yarn add @headlessui-float/vue

Create the following composable called useHoverMenu.ts in your project.

export const useHoverMenu = (delay = 350, openDelay = 200) => {
  const show = ref(false);
  const closeTimer = ref(null) as Ref<ReturnType<typeof setTimeout> | null>;
  const openTimer = ref(null) as Ref<ReturnType<typeof setTimeout> | null>;

  const open = () => {
    if (closeTimer.value) {
      clearTimeout(closeTimer.value);
      closeTimer.value = null;
    }
    openTimer.value = setTimeout(() => {
      show.value = true;
      openTimer.value = null;
    }, openDelay);
  };

  const close = () => {
    if (openTimer.value) {
      clearTimeout(openTimer.value);
      openTimer.value = null;
    }
    closeTimer.value = setTimeout(() => {
      show.value = false;
      closeTimer.value = null;
    }, delay);
  };

  return { show, closeTimer, openTimer, open, close };
};

Copy the following code into your project.

<template>
  <HPopover ref="parent" @mouseleave="hover && close()">
    <Float :offset="10" flip shift :placement="placement" :show="show" transition-name="popover">
      <HPopoverButton @click="show = !show" as="div" @mouseover="hover && open()">
        <slot :close="close" :open="open" :show="show"></slot>
      </HPopoverButton>
      <HPopoverPanel @mouseover="hover && open()" static>
        <slot name="content" :close="close" :open="open" :show="show"></slot>
      </HPopoverPanel>
    </Float>
  </HPopover>
</template>

<script setup lang="ts">
  import { Float, type FloatProps } from "@headlessui-float/vue";
  const props = withDefaults(
    defineProps<{
      openDelay?: number;
      closeDelay?: number;
      hover?: boolean;
      placement?: FloatProps["placement"];
    }>(),
    {
      openDelay: 50,
      closeDelay: 250,
      placement: "bottom",
    }
  );

  const { show, open, close } = useHoverMenu(toValue(props.closeDelay), toValue(props.openDelay));

  const parent = ref(null) as Ref<HTMLElement | null>;
  onClickOutside(parent, () => {
    close();
  });
  defineExpose({ close, open });
</script>

<style scoped>
  .popover-enter-active,
  .popover-leave-active {
    transition: all 200ms ease-in-out;
  }

  .popover-enter-from,
  .popover-leave-to {
    opacity: 0;
    transform: scale(0.95);
  }
</style>