v-popover
TypeScript icon, indicating that this package has built-in type declarations

0.1.13 • Public • Published

Popover Plugin for Vue 3

A tooltip and popover plugin for Vue.js.

Vue.js 3.2+ required.

  • Based on Floating UI for smart positioning
  • Supports triggers and content that are colocated or remotely positioned
  • Supports multiple triggers with shared content component
  • Supports Vue teleport under the hood
  • Focus management built in

Usage

Tooltips

Basic Tooltip

Tooltip components can display a simple content message.

<template>
  <Tooptip content="This is a very important message">
    <BasicButton>Hover Me</BasicButton>
  </Tooptip>
</template>

Tooltip with options

Tooltips accept multiple props that override the default tooltip behavior.

<template>
  <Tooltip
    content="This is a very important message"
    placement="bottom-start"
    action="click"
    >
    <BaseButton>Click Me</BaseButton>
  </Tooltip>
</template>

Popovers

Popovers are used to display dynamic, interactive content over your UI. They have different styling and behavior than tooltips, but can be further customized using props.

Popovers require a trigger element and custom content to display.

Popover Triggers

Triggers are defined by either a PopoverTrigger component or a v-popover directive.

The difference between these options is that while a PopoverTrigger component may be linked with an external PopoverContent component, it also provides a content slot to conveniently display popover content inline.

Alternatively, using a directive requires the use of an external PopoverContent component to display its popover content.

Single trigger

Consider the use of a single trigger that is colocated with its popover content.

A PopoverTrigger may define its content in a slot, while the directive requires use of a separate PopoverContent component, linked by a common name.

<!--Component trigger-->
<template>
  <PopoverTrigger>
    <BaseButton>
      <IconSettings class="mr-2 w-4 h-4" />Settings
    </BaseButton>
    <template #content>
      <div class="rounded-md space-y-1">
        <BaseButton transparent>Account Settings</BaseButton>
        <BaseButton transparent>Support</BaseButton>
        <BaseButton transparent>License</BaseButton>
        <BaseButton transparent>Sign out</BaseButton>
      </div>
    </template>
  </PopoverTrigger>
</template>
<!--Directive trigger-->
<template>
  <BaseButton v-popover="{ name: 'menu' }">
    <IconSettings class="mr-2 w-4 h-4" />Settings
  </BaseButton>
  <PopoverContent name="menu">
      <div class="rounded-md space-y-1">
        <BaseButton transparent>Account Settings</BaseButton>
        <BaseButton transparent>Support</BaseButton>
        <BaseButton transparent>License</BaseButton>
        <BaseButton transparent>Sign out</BaseButton>
      </div>
  </PopoverContent>
</template>

The extra complexity introduced by having to use a separate component linked by a common name likely makes the directive a less attractive option for this simple use case.

Multiple triggers

When the popover trigger and content are not colocated, or a single content section needs be shared between multiple triggers, a PopoverContent component is required with a name prop.

The same name prop must be set on the PopoverTarget components or v-popover directives to link the triggers and content.

Common popover options may be set on PopoverContent via props, but triggers may override these options with their own props.

<!--Component trigger-->
<script setup>
import { ref } from 'vue';

const menu = ref([
  { label: 'Products', opts: { data: 'products' } },
  { label: 'Resources', opts: { data: 'resources' } },
  { label: 'Pricing', opts: { data: 'pricing', placement: 'bottom-end' } },
  { label: 'Settings', opts: { data: 'settings', placement: 'bottom-end' } },
]);
</script>

<template>
  <div class="flex gap-2">
    <PopoverTrigger
      v-for="{ label, opts } in menu"
      :key="label"
      v-bind="{ ...opts, name: 'shared_popover' }"
    >
      <BaseButton transparent>
        {{ label }}
      </BaseButton>
    </PopoverTrigger>
  </div>
  <PopoverContent name="shared_popover" v-slot="{ data }">
    <div class="space-y-1">
      <template v-if="data === 'products'">
        <BaseButton transparent>Product A</BaseButton>
        <BaseButton transparent>Product B</BaseButton>
        <BaseButton transparent>Product C</BaseButton>
      </template>
      <template v-else-if="data === 'resources'">
        <BaseButton transparent>Resource A</BaseButton>
        <BaseButton transparent>Resource B</BaseButton>
        <BaseButton transparent>Resource C</BaseButton>
        <BaseButton transparent>Resource D</BaseButton>
      </template>
      <template v-else-if="data === 'pricing'">
        <BaseButton transparent>Pricing A</BaseButton>
        <BaseButton transparent>Pricing B</BaseButton>
      </template>
      <template v-else-if="data === 'settings'">
        <BaseButton transparent>Account Settings</BaseButton>
        <BaseButton transparent>Support</BaseButton>
        <BaseButton transparent>License</BaseButton>
        <BaseButton transparent>Sign out</BaseButton>
      </template>
    </div>
  </PopoverContent>
</template>

Notice how we assign specific data for each trigger. This data can be any value, and is passed through to the PopoverContent slot when triggered in order to display separate user interfaces, depending on which trigger is active.

Open state

Use the open slot prop to detect if the popover is open for a particular PopoverTrigger.

<template>
  <PopoverTrigger :transitions="['fade', 'slide']">
    <template #default="{ open }">
      <BaseButton transparent>
        Settings
        <IconChevronDown v-if="!open" class="ml-2 w-3.5 h-3.5" />
        <IconChevronUp v-else class="ml-2 w-3.5 h-3.5" />
      </BaseButton>
    </template>
    <template #content>
      <div class="rounded-md space-y-1">
        <BaseButton transparent>Account Settings</BaseButton>
        <BaseButton transparent>Support</BaseButton>
        <BaseButton transparent>License</BaseButton>
        <BaseButton transparent>Sign out</BaseButton>
      </div>
    </template>
  </PopoverTrigger>
</template>

Transitions

By default, when PopoverContent is shared between multiple triggers, the content pane will transition between the trigger elements as the trigger actions occur.

To disable this behavior, the move option can be omitted from the transitions array.

Focus management

When a popover is shown with the click action, focus is placed inside it, as it is a focus trap. A user cannot tab outside of the popover until it is closed by an outside click or the escape key. If the popover has focus when it is closed, focus is placed back on the trigger element.

Events

Overview

There are multiple ways to register for popover events.

Generally, there are 3 types of events. Each event includes a readonly copy of the popover's state in the payload.

Show: Sent when a popover is opened.

Hide: Sent when a popover is hidden.

Update: Sent when a popover's state is updated. Included in the payload is an updates object with the updated state properties as keys mapped to their old and new values in an array.

// Update event payload
{
  // Current popover state
  direction: 'top',
  alignment: 'right',
  // ...
  updates: {
    direction: ['bottom', 'top'],
    alignment: ['left', 'right'],
    // ...
  }
}

Events may be registered directly on components or globally via helper methods included with the plugin.

Component Events

Event listeners for all event types can be directly set on PopoverTrigger and PopoverContent components.

If a PopoverContent component is shared across multiple triggers, and it is transitioning from one trigger to another, a separate hide event is emitted for the old trigger and a show event is emitted for the new trigger.

Global Events

If you don't have direct access to PopoverTrigger or PopoverContent components, events can be registered globally on the document via popovershow, popoverhide and popoverupdate.

Using onPopoverEvent()

Import the onPopoverEvent helper to easily register handlers for these events. This helper returns a cleanup function that can be called at a later time.

import { onPopoverEvent } from 'v-popover';

const off = onPopoverEvent('popovershow', (e) => {
  console.log('Popover displayed for', e.detail.name);
});

// Cleanup later
off();

Since this callback will get called for every popover in your app, you can check for specific popovers within the callback, or pass an optional filter object as the third parameter.

import { onPopoverEvent } from 'v-popover';

// Method 1: Filter out events within callback
onPopoverEvent('popovershow', (e) => {
  if (e.detail.name === 'my_popover') {
    console.log('Popover displayed for', e.detail.name);
  }
});

// Method 2: Filter out events with optional filter
onPopoverEvent('popovershow', (e) => {
  console.log('Popover displayed for', e.detail.name);
}, {
  name: 'my_popover',
  // Other specific conditions
  id: 'my_popover_trigger',
});

// Use `updates` to only get callbacks for specific state updates
onPopoverEvent('popoverupdate', (e) => {
  console.log('Direction changed for popover', e.detail.name);
}, {
  name: 'my_popover',
  id: 'my_popover_trigger',
  // Only updates to these state keys will trigger callback
  updates: ['direction'],
})
Using usePopoverEvent()

Within components, you can call usePopoverEvent to automatically register the callback in onMounted and unregister the callback in onUnmounted.

Otherwise, this function behaves exactly the same as onPopoverEvent().

<script setup>
import { usePopoverEvent } from 'v-popover';

usePopoverEvent('popoverupdate', (e) => {
  console.log('Direction changed for popover', e.detail.name);
}, {
  name: 'my_popover',
  id: 'my_popover_trigger',
  updates: ['direction'],
})
</script>

Styling

VPopover provides customized styling support via the theme and contentClass properties.

Use theme

Basic light/dark mode styling can be optionally applied by setting the theme property on components or the directives.

For example, the Tooltip component uses the dark theme under the hood, but can be reset to the light theme by manually.

<template>
  <div class="flex gap-2">
    <Tooltip content="I am dark">
        <BaseButton>Dark Tooltip</BaseButton>
    </Tooltip>
    <Tooltip theme="light" content="I am light">
        <BaseButton>Light Tooltip</BaseButton>
    </Tooltip>
  </div>
</template>
Clearing theme

A theme can be cleared by setting theme to a falsey value. This will clear all of the default styling for the popover content.

<template>
  <Tooltip content="I have no styling applied" theme="">
      <BaseButton>Dark Tooltip</BaseButton>
  </Tooltip>
</template>

Since the theme is now cleared, custom styling can be easily applied using contentClass.

Use contentClass

The contentClass property can be used to apply a custom class to the popover content window.

<template>
  <Tooltip
    content="WARNING: This is a dangerous operation"
    content-class="font-bold bg-red-100 text-red-600 px-2 py-1 border border-red-300 rounded-lg"
    :arrow-size="14"
    theme=""
  >
      <BaseButton class="bg-red-500 hover:bg-red-700">Dangerous Action</BaseButton>
  </Tooltip>
</template>

Installation

Vue.js 3.2+ is required

Install Plugin

// npm
npm install v-popover

// yarn
yarn add v-popover

Use Plugin

As of v3.0.0-alpha.7, all installation methods require manual import of component styles. This is due to Vite build restrictions in libary mode.

import 'v-popover/style.css';

Method 1: Use Globally

// main.js
import VPopover from 'v-popover';
import 'v-popover/style.css';

// Use plugin with optional defaults
app.use(VPopover, {})
<!--MyComponent.vue-->
<template>
  <VPopover />
</template>

Method 2: Use Components Globally

// main.js
import { PopoverTrigger, PopoverContent } from 'v-popover';
import 'v-popover/style.css';

// Use plugin defaults (optional)
app.use(setupCalendar, {})

// Use the components
app.component('PopoverTrigger', PopoverTrigger)
app.component('PopoverContent', PopoverContent)
<!--MyComponent.vue-->
<template>
  <PopoverTrigger />
  <PopoverContent />
</template>

Method 3: Use Components As Needed

<!--MyComponent.vue-->
<template>
  <PopoverTrigger>
    <button type="button">Show content</button>
  <PopoverTrigger>
  <PopoverContent>
    Custom content here
  </PopoverContent>
  <DatePicker v-model="date">
</template>

<script setup>
import { PopoverTrigger, PopoverContent } from 'v-popover';
import 'v-popover/style.css';
</script>

Package Sidebar

Install

npm i v-popover

Homepage

vpopover.io

Weekly Downloads

19

Version

0.1.13

License

MIT

Unpacked Size

1.2 MB

Total Files

51

Last publish

Collaborators

  • nathanreyes