funkwhale/front/ui-docs/components/ui/popover.md

22 KiB

import Popover from "~/components/ui/Popover.vue"

Popover (Dropdown Menu)

Popovers (Popover) are dropdown menus activated by a button. Use popovers to create dropdown menus ranging from simple lists to complex nested menus.

::: warning A11y issues

This component has severe usability issues and cannot be used as-is.

Add keyboard operability

I can't operate the popup with a keyboard. Remove barrier for people not using a mouse.

  • All items can be focused and activated by Space (use <button> element instead of <div>)

  • Tab order: Users can go to the next and previous items with Tab and Shift+Tab. When they have opened a sub-menu, the next focusable element is inside the sub-menu. When they have closed it, the focus jumps back to where it was.

  • Dismissal: Users can close a menu or sub-menu with ESC

  • Arrow keys: Users can move up and down a menu, open sub-menus with -> and close them with <-. I have added something like this to Tabs.vue, for reference.

Implement expected behavior

Switching to submenus is error-prone. When moving cursor into freshly opened submenu, it should not close if the cursor crosses another menu item.

  • Dead triangle: Add a triangular invisible node that covers the possible paths from the current mouse position to the menu items.

Large menus disappear. I can't scroll to see all options.

  • Submenu-to-Modal: Lists longer than 12 or so items are not recommended and should be replaced with modals.

Submenus open without a delay, and they don't close unless I click somewhere outside them, which goes against established expectations.

  • Expansion delay: Sub-menus open after 200ms

  • Auto-close: Sub-menus close when the outside is hovered. There may be a delay of 200ms. Menus close when they lose focus.


Common UI libraries in the Vue ecosystem such as vuetify or shadcn-vue all implement these features. It may be prudent to use their components.

::: tip Quick mitigation tactics:

:::

Common uses:

  • "More actions" dropdown menus
  • Navigation menus
  • Settings menus
  • Context menus (right-click menus)
Prop Data type Required? Description
open Boolean No Controls whether the popover is open. Defaults to false.

toc

<Popover>
  <template #default="{ toggleOpen }">
    <OptionsButton @click="toggleOpen" />
  </template>
</Popover>

Destructure the function toggleOpen and let a default dropdown button: OptionsButton trigger it. This way, the state of the component is encapsulated.

Bind to isOpen

If you want to process or influence the expansion of the menu in the containing component, you can bind it to a ref.

Use Vue event handling to map the button to a boolean value.

<script setup lang="ts">
  const isOpen = ref(false)
</script>

<template>
  <Popover v-model="isOpen">
    <OptionsButton @click="isOpen = !isOpen" />
  </Popover>
</template>

Customize the dropdown button

<script setup lang="ts">
const open = ref(false);
const privacyChoices = ["pod", "public", "private"];
const bcPrivacy = ref("pod");
</script>

<template>
  <Popover v-model="isOpen">
    <template #default="{ toggleOpen }">
      <Pill
        @click="
          (e) => {
            console.log('Pill clicked');
            console.log('Before toggleOpen:', isOpen);
            toggleOpen();
            console.log('After toggleOpen:', isOpen);
          }
        "
        :blue="bcPrivacy === 'pod'"
        :red="bcPrivacy === 'public'"
      >
        {{ bcPrivacy }}
      </Pill>
    </template>
    <template #items>
      <PopoverRadio v-model="bcPrivacy" :choices="privacyChoices" />
    </template>
  </Popover>
</template>
{{ bcPrivacy }}

Items

Popovers contain a list of menu items. Items can contain different information based on their type.

::: info Lists of items must be nested inside a <template #items> tag directly under the <Popover> tag. :::

Popover item

The popover item (PopoverItem) is a simple button that uses Vue event handling. Each item contains a slot which you can use to add a menu label and icon.

<script setup lang="ts">
  const alert = (message: string) => window.alert(message)
  const open = ref(false)
</script>

<template>
  <Popover v-model="open">
    <OptionsButton @click="open = !open" />
    <template #items>
      <PopoverItem @click="alert('Report this object?')">
        <i class="bi bi-exclamation" />
        Report
      </PopoverItem>
    </template>
  </Popover>
</template>
Report

Checkbox

The checkbox (PopoverCheckbox) is an item that acts as a selectable box. Use v-model to bind the checkbox to a boolean value. Each checkbox contains a slot which you can use to add a menu label.

<script setup lang="ts">
const bc = ref(false)
const cc = ref(false)
const open = ref(false)
</script>

<template>
  <Popover v-model="open">
    <OptionsButton @click="open = !open" />
    <template #items>
      <PopoverCheckbox v-model="bc">
        Bandcamp
      </PopoverCheckbox>
      <PopoverCheckbox v-model="cc">
        Creative commons
      </PopoverCheckbox>
    </template>
  </Popover>
</template>
Bandcamp Creative commons

Radio

The radio (PopoverRadio) is an item that acts as a radio selector.

Prop Data type Required? Description
modelValue String Yes The current value of the radio. Use v-model to bind this to a value.
choices Array<String> Yes A list of choices.
<script setup lang="ts">
const open = ref(false);
const currentChoice = ref("pod");
const privacy = ["public", "private", "pod"];
</script>

<template>
  <Popover v-model="open">
    <OptionsButton @click="open = !open" />
    <template #items>
      <PopoverRadio v-model="currentChoice" :choices="choices" />
    </template>
  </Popover>
</template>

Separator

Use a standard horizontal rule (<hr>) to add visual separators to popover lists.

<script setup lang="ts">
const bc = ref(false)
const cc = ref(false)
const open = ref(false)
</script>

<template>
  <Popover v-model="open">
    <OptionsButton @click="open = !open" />
    <template #items>
      <PopoverCheckbox v-model="bc">
        Bandcamp
      </PopoverCheckbox>
      <hr>
      <PopoverCheckbox v-model="cc">
        Creative commons
      </PopoverCheckbox>
    </template>
  </Popover>
</template>
Bandcamp
Creative commons

Icon Prop

PopoverItem supports an icon prop to easily add icons to menu items. The icon prop accepts standard Bootstrap icon classes.

Prop Data type Required? Description
icon String No Bootstrap icon class to display before the item
<PopoverItem icon="bi-music-note-list">
  Play next
</PopoverItem>

<PopoverItem icon="right bi-share">
  Share
</PopoverItem>
Play next Share ```

Submenus

To create more complex menus, you can use submenus (PopoverSubmenu). Submenus are menu items which contain other menu items.

<script setup lang="ts">
const bc = ref(false)
const isOpen = ref(false)
</script>

<template>
  <Popover v-model="isOpen">
    <OptionsButton @click="isOpen = !isOpen" />
    <template #items>
      <PopoverSubmenu>
        <i class="bi bi-collection" />
        Organize and share
        <template #items>
          <PopoverCheckbox v-model="bc">
            Bandcamp
          </PopoverCheckbox>
        </template>
      </PopoverSubmenu>
    </template>
  </Popover>
</template>
Organize and share Bandcamp

Extra items

You can add extra items to the right hand side of a popover item by nesting them in a <template #after> tag. Use this to add additional menus or buttons to menu items.

<script setup lang="ts">
const bc = ref(false)
const privacyChoices = ['public', 'private', 'pod']
const bcPrivacy = ref('pod')
const isOpen = ref(false)
</script>

<template>
  <Popover v-model="isOpen">
    <OptionsButton @click="isOpen = !isOpen" />
    <template #items>
      <PopoverSubmenu>
        <i class="bi bi-collection" />
        Organize and share
        <template #items>
          <PopoverCheckbox v-model="bc">
            Bandcamp
            <template #after>
              <Popover>
                <template #default="{ toggleOpen }">
                  <Pill @click.stop="toggleOpen" :blue="bcPrivacy === 'pod'" :red="bcPrivacy === 'public'">
                    {{ bcPrivacy }}
                  </Pill>
                </template>
                <template #items>
                  <PopoverRadio v-model="bcPrivacy" :choices="privacyChoices"/>
                </template>
              </Popover>
            </template>
          </PopoverCheckbox>
          <hr>
          <PopoverCheckbox v-model="share">
            Share by link
            <template #after>
              <Button ghost @click.stop="alert('Link copied to clipboard')" round icon="bi-link" />
              <Button ghost @click.stop="alert('Here is your code')" round icon="bi-code" />
            </template>
          </PopoverCheckbox>
        </template>
      </PopoverSubmenu>
    </template>
  </Popover>
</template>
Organize and share Bandcamp {{ bcPrivacy }}
Share by link

You can use PopoverItems as Links by providing a to prop with the route object or and external Url (http...). Read more on the Link component page.

Hello Change language {{ language }}

Menu

Here is an example of a completed menu containing all supported features.

<script setup lang="ts">
const isOpen = ref(false);
const bc = ref(false);
const cc = ref(false);
const share = ref(false);
const bcPrivacy = ref("pod");
const ccPrivacy = ref("public");
const privacyChoices = ["private", "pod", "public"];
</script>

<template>
  <Popover v-model="isOpen">
    <OptionsButton @click="isOpen = !isOpen" />
    <template #items>
      <PopoverSubmenu>
        <i class="bi bi-music-note-list" />
        Change language
        <template #items>
          <PopoverItem
            v-for="(language, key) in SUPPORTED_LOCALES"
            :key="key"
            @click="setI18nLanguage(key)"
          >
            {{ language }}
          </PopoverItem>
        </template>
      </PopoverSubmenu>
      <PopoverItem>
        <i class="bi bi-arrow-up-right" />
        Play next
      </PopoverItem>
      <PopoverItem>
        <i class="bi bi-arrow-down-right" />
        Append to queue
      </PopoverItem>
      <PopoverSubmenu>
        <i class="bi bi-music-note-list" />
        Add to playlist
        <template #items>
          <PopoverItem>
            <i class="bi bi-music-note-list" />
            Sample playlist
          </PopoverItem>
          <hr />
          <PopoverItem>
            <i class="bi bi-plus-lg" />
            New playlist
          </PopoverItem>
        </template>
      </PopoverSubmenu>
      <hr />
      <PopoverItem>
        <i class="bi bi-heart" />
        Add to favorites
      </PopoverItem>
      <PopoverSubmenu>
        <i class="bi bi-collection" />
        Organize and share
        <template #items>
          <PopoverCheckbox v-model="bc">
            Bandcamp
            <template #after>
              <Popover>
                <template #default="{ toggleOpen }">
                  <Pill
                    @click.stop="toggleOpen"
                    :blue="bcPrivacy === 'pod'"
                    :red="bcPrivacy === 'public'"
                  >
                    {{ bcPrivacy }}
                  </Pill>
                </template>
                <template #items>
                  <PopoverRadio v-model="bcPrivacy" :choices="privacyChoices" />
                </template>
              </Popover>
            </template>
          </PopoverCheckbox>
          <PopoverCheckbox v-model="cc">
            Creative Commons
            <template #after>
              <Popover v-model="isOpen">
                <template #default="{ toggleOpen }">
                  <Pill
                    @click="toggleOpen"
                    :blue="ccPrivacy === 'pod'"
                    :red="ccPrivacy === 'public'"
                  >
                    {{ ccPrivacy }}
                  </Pill>
                </template>
                <template #items>
                  <PopoverRadio v-model="ccPrivacy" :choices="privacyChoices" />
                </template>
              </Popover>
            </template>
          </PopoverCheckbox>
          <hr />
          <PopoverItem>
            <i class="bi bi-plus-lg" />
            New library
          </PopoverItem>
          <hr />
          <PopoverCheckbox v-model="share">
            Share by link
            <template #after>
              <Button @click.stop ghost round icon="bi-link" />
              <Button @click.stop ghost round icon="bi-code" />
            </template>
          </PopoverCheckbox>
        </template>
      </PopoverSubmenu>
      <PopoverItem>
        <i class="bi bi-cloud-download" />
        Download
      </PopoverItem>
      <hr />
      <PopoverItem>
        <i class="bi bi-exclamation" />
        Report
      </PopoverItem>
    </template>
  </Popover>
</template>
Play next Append to queue Change language {{ language }} Add to playlist Sample playlist
New playlist

Add to favorites Organize and share Bandcamp {{ bcPrivacy }} Creative Commons {{ ccPrivacy }}
New library
Share by link
Download
Report