可重复使用的弹出框以添加一些弹出效果

Avatar of Mateusz Rybczonek
Mateusz Rybczonek

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

弹出框是一个短暂的视图,当用户点击控制按钮或在定义区域内时,它会显示在屏幕上的内容之上。例如,点击特定列表项上的信息图标以获取项目详细信息。通常,弹出框包含一个指向其出现位置的箭头。

弹出框非常适合我们想要显示临时上下文以在与屏幕上的特定元素交互时引起用户注意的情况。它们为用户提供额外的上下文和说明,而无需使屏幕杂乱无章。用户只需通过点击打开它们的方式或点击弹出框外部来关闭它们。

我们将研究一个名为 popper.js 的库,它允许我们在 Vue 框架中创建可重复使用的弹出框组件。弹出框是像 Vue 这样的基于组件的系统中组件的完美类型,因为它们可以是独立维护的包含、封装的组件,但在整个应用程序中使用。

让我们深入研究并开始。

但首先:弹出框和工具提示有什么区别?

是“弹出框”这个名称让您感到困惑吗?事实上,弹出框非常类似于工具提示,工具提示是另一种常见的 UI 模式,用于在包含的元素中显示其他上下文。但是,它们之间也存在差异,所以让我们简要说明一下,以便我们清楚地了解我们正在构建的内容。

工具提示 弹出框
工具提示就是指工具或其他交互的提示或建议。它们旨在澄清或帮助您使用它们悬停的内容,而不是添加其他内容。 另一方面,弹出框可以更加详细,它们可以在正文中包含标题和多行文本。
工具提示通常只在悬停时可见,因此,如果您需要在与页面其他部分进行交互时能够阅读内容,那么工具提示将不起作用。 弹出框通常是可关闭的,无论是通过点击页面其他部分还是第二次点击弹出框目标(取决于实现方式),因此您可以设置一个弹出框,让您能够与页面上的其他元素进行交互,同时仍然能够阅读它的内容。

弹出框最适合在较大的屏幕上使用,我们最有可能在以下用例中遇到它们:

查看这些用例,我们可以收集一些构成良好弹出框的要求:

  1. 可重复使用性:弹出框应该允许传递自定义内容到弹出框。
  2. 可关闭性:弹出框应该可以通过点击弹出框外部和退出按钮来关闭。
  3. 定位:弹出框应该在到达屏幕边缘时重新定位。
  4. 交互:弹出框应该允许与弹出框中的内容进行交互。

我创建了一个示例,供我们在创建组件的过程中参考。

查看演示

好了,现在我们已经对弹出框以及我们正在构建的内容有了基本了解,让我们进入使用 popper.js 创建弹出框的步骤。

步骤 1:创建 BasePopover 组件

让我们从创建一个负责初始化和定位弹出框的组件开始。我们将这个组件命名为 BasePopover.vue,在组件模板中,我们将渲染两个元素:

  • 弹出框内容:这是负责渲染弹出框内内容的元素。现在,我们使用一个插槽,它将允许我们从负责渲染弹出框的父组件传递内容(要求 #1:可重复使用性)。
  • 弹出框覆盖层:这是负责覆盖弹出框下内容并阻止用户与弹出框外部元素进行交互的元素。它还允许我们在点击时关闭弹出框(要求 #2:可关闭性)。
// BasePopover.vue
<template>
  <div>
    <div
      ref="basePopoverContent"
      class="base-popover"
    >
      <slot />
    </div>
    <div
      ref="basePopoverOverlay"
      class="base-popover__overlay"
    />
  </div>
</template>

在组件的脚本部分:

  • 我们导入 popper.js(负责弹出框定位的库),然后
  • 我们接收 popoverOptions 道具,最后
  • 我们将初始 popperInstance 设置为 null(因为最初我们没有弹出框)。

让我们描述 popoverOptions 对象包含的内容:

  • popoverReference:这是相对于弹出框将要定位的元素的对象(通常是触发弹出框的元素)。
  • placement:这是一个 popper.js 放置选项,它指定弹出框相对于弹出框参考元素(它所附加的元素)的位置。
  • offset:这是一个 popper.js 偏移修饰符,它允许我们通过传递 x 和 y 坐标来调整弹出框位置。
import Popper from "popper.js"

export default {
  name: "BasePopover",

  props: {
    popoverOptions: {
      type: Object,
      required: true
    }
  },

  data() {
    return {
      popperInstance: null
    }
  }
}

为什么我们需要它?popper.js 库允许我们轻松地相对于另一个元素定位元素。它还在弹出框到达屏幕边缘时执行神奇操作,并重新定位它,使其始终位于用户的视窗内(要求 #3:定位)。

步骤 2:初始化 popper.js

现在我们有了 BasePopover 组件骨架,我们将添加几个方法,它们将负责定位和显示弹出框。

initPopper 方法中,我们将首先创建一个 modifiers 对象,它将用于创建 Popper 实例。我们将从父组件接收的选项(placementoffset)设置到 modifiers 对象中的对应字段。所有这些字段都是可选的,这就是为什么我们首先需要检查它们是否存在。

然后,我们通过传递以下内容来初始化一个 新的 Popper 实例:

  • popoverReference 节点(弹出框指向的元素:popoverReference 引用)
  • 弹出框内容节点(包含弹出框内容的元素:basePopoverContent 引用)
  • options 对象

我们还设置了 preventOverflow 选项,以防止弹出框定位在视窗之外。初始化后,我们将弹出框实例设置为我们的 popperInstance 数据属性,以便将来能够访问 popper.js 提供的方法和属性。

methods: {
...
  initPopper() {
    const modifiers = {}
    const { popoverReference, offset, placement } = this.popoverOptions
  
    if (offset) {
      modifiers.offset = {
        offset
      }
    }
  
    if (placement) {
      modifiers.placement = placement
    }
  
    this.popperInstance = new Popper(
      popoverReference,
      this.$refs.basePopoverContent,
      {
        placement,
        modifiers: {
          ...modifiers,
          preventOverflow: {
            boundariesElement: "viewport"
          }
        }
      }
    )
  }
...
}

现在我们有了 initPopper 方法,我们需要一个地方来调用它。最适合的地方是在 mounted 生命周期钩子中。

mounted() {
  this.initPopper()
  this.updateOverlayPosition()
}

如您所见,我们在 mounted 钩子中还调用了一个方法:updateOverlayPosition 方法。此方法是一种安全措施,用于重新定位我们的覆盖层,以防我们在页面上还有其他具有绝对定位的元素(例如 NavBarSideBar)。此方法确保覆盖层始终覆盖整个屏幕,并阻止用户与除弹出框和覆盖层本身之外的任何元素进行交互。

methods: {
...
  updateOverlayPosition() {
    const overlayElement = this.$refs.basePopoverOverlay;
    const overlayPosition = overlayElement.getBoundingClientRect();
  
    overlayElement.style.transform = <code>translate(-${overlayPosition.x}px, -${
      overlayPosition.y
    }px)`;
  }
...
}

步骤 3:销毁 Popper

我们已经初始化了弹出框,但现在我们需要一种方法,在弹出框关闭时将其移除和释放。在这种情况下,无需将其保留在 DOM 中。

当我们点击弹出框外部的任何地方时,我们希望关闭弹出框。我们可以通过向覆盖层添加点击侦听器来做到这一点,因为我们确保覆盖层始终覆盖弹出框下方的整个屏幕。

<template>
...
  <div
    ref="basePopoverOverlay"
    class="base-popover__overlay"
    @click.stop="destroyPopover"
  />
...
</template>

让我们创建一个负责销毁弹出框的方法。在这个方法中,我们首先检查 popperInstance 是否实际存在,如果存在,我们调用 popper 的 destroy 方法,确保弹出框实例被销毁。之后,我们通过将其设置为 null 来清理 popperInstance 数据属性,并发出一个 closePopover 事件,该事件将在负责渲染弹出框的组件中处理。

methods: {
...
  destroyPopover() {
      if (this.popperInstance) {
        this.popperInstance.destroy();
        this.popperInstance = null;
        this.$emit("closePopover");
      }
    }
...
}

步骤 4:渲染 BasePopover 组件

好的,我们已经准备好了要渲染的弹出框。我们在父组件中进行渲染,父组件将负责管理弹出框的可见性并将内容传递给它。

在模板中,我们需要有一个元素负责触发我们的弹出框(popoverReference)和 BasePopover 组件。BasePopover 组件接收一个 popoverOptions 属性,该属性将告诉组件我们希望如何显示它,以及一个绑定到 v-if 指令的 isPopoverVisible 属性,该属性将负责显示和隐藏弹出框。

<template>
  <div>
    <img
      ref="popoverReference"
      width="25%"
      src="./assets/logo.png"
    >
    <BasePopover
      v-if="isPopoverVisible"
      :popover-options="popoverOptions"
    >
      <div class="custom-content">
        <img width="25%" src="./assets/logo.png">
        Vue is Awesome!
      </div>
    </BasePopover>
  </div>
</template>

在组件的脚本部分,我们导入 BasePopover 组件,将 isPopoverVisible 标志初始设置为 false,并将 popoverOptions 对象用于在初始化时配置弹出框。

data() {
  return {
    isPopoverVisible: false,
    popoverOptions: {
      popoverReference: null,
      placement: "top",
      offset: "0,0"
    }
  };
}

我们最初将 popoverReference 属性设置为 null,因为当父组件创建时,将作为弹出框触发器的元素不存在。我们在 mounted 生命周期钩子中修复了这个问题,此时组件(和弹出框引用)已渲染。

mounted() {
  this.popoverOptions.popoverReference = this.$refs.popoverReference;
}

现在,让我们创建两个方法,openPopoverclosePopover,它们将负责通过在 isPopoverVisible 属性上设置适当的值来显示和隐藏我们的弹出框。

methods: {
  closePopover() {
    this.isPopoverVisible = false;
  },
  openPopover() {
    this.isPopoverVisible = true;
  }
}

在这一步中,我们需要做的最后一件事是将这些方法附加到模板中的相应元素。我们将 openPopover 方法附加到触发元素的点击事件,并将 closePopover 方法附加到 BasePopover 组件发出的 closePopover 事件,当弹出框在点击弹出框覆盖层时被销毁时,该事件会被触发。

<template>
  <div>
    <img
      ...
      @click="openPopover"
    >
    <BasePopover
      ...
      @closePopover="closePopover"
    >
      ...
    </BasePopover>
  </div>
</template>

有了这些,我们就可以在点击触发元素时显示弹出框,并在点击弹出框外部时隐藏弹出框。

步骤 5:创建 BasePopoverContent 组件

虽然它看起来不像一个弹出框。当然,它渲染了传递给 BasePopover 组件的内容,但它没有使用通常的弹出框包装器和指向触发元素的箭头来进行渲染。我们可以在 BasePopover 组件中包含包装器组件,但这会使其可重用性降低,并将弹出框与特定模板实现耦合在一起。我们的解决方案允许我们在弹出框中渲染任何模板。我们还想确保组件只负责定位和显示内容。

为了使它看起来像一个弹出框,让我们创建一个 BasePopoverContent 组件。我们需要在模板中渲染两个元素

  • 一个箭头元素,它有一个 popper.js x-arrow 选择器,popper.js 需要该选择器才能正确定位箭头
  • 内容包装器,它公开一个插槽元素,我们的内容将在其中渲染
<template>
  <div class="base-popover-content">
    <div class="base-popover-content__arrow" x-arrow/>
    <div class="base-popover-content__body">
      <slot/>
    </div>
  </div>
</template>

现在让我们在使用 BasePopover 的父组件中使用我们的包装器组件

<template>
  <div>
    <img
      ref="popoverReference"
      width="25%"
      src="./assets/logo.png"
      @click="openPopover"
    >
    <BasePopover
      v-if="isPopoverVisible"
      :popover-options="popoverOptions"
      @closePopover="closePopover"
    >
      <BasePopoverContent>
        <div class="custom-content">
          <img width="25%" src="./assets/logo.png">
          Vue is Awesome!
        </div>
      </BasePopoverContent>
    </BasePopover>
  </div>
</template>

就这样!

您可以看到上面的示例中弹出框的动画效果,动画效果会进进出出。为了简洁起见,我们在这篇文章中省略了动画效果,但您可以查看 其他 popper.js 示例 寻找灵感。

您可以在这里看到动画代码和工作示例 here.

让我们看看我们的需求,看看我们是否错过了什么

通过? 需求 解释
通过 可重用性 我们在 BasePopover 组件中使用了插槽,它将弹出框实现与内容模板解耦。这使我们能够将任何内容传递给组件。
失败 可关闭性 我们使点击弹出框外部时关闭弹出框成为可能。我们仍然需要确保能够通过按下键盘上的 ESC 来关闭弹出框。
通过 定位 这就是 popper.js 为我们解决了一个问题的地方。它不仅赋予了我们定位能力,而且还负责在弹出框到达视窗边缘时重新定位弹出框。
失败 交互 我们有一个进进出出的弹出框,但我们还没有与弹出框内容进行任何交互。就目前而言,它看起来更像一个工具提示,而不是弹出框,实际上,它可以在显示和隐藏元素时用作工具提示。工具提示通常在悬停时显示,所以这是我们唯一需要更改的地方。

糟糕,我们没有满足交互需求。添加交互是创建一个(或多个)组件的问题,这些组件将被放置在 BasePopoverContent 插槽中。在示例中,我创建了一个非常简单的组件,它有一个标题和文本,显示了一些 Vue 样式指南规则。通过点击按钮,我们可以与弹出框内容进行交互并更改规则,当你到达最后一个规则时,按钮会改变其用途,并充当弹出框的关闭按钮。它非常类似于我们在应用程序中看到的新的用户欢迎屏幕。

我们还需要完全满足可关闭性需求。除了点击弹出框外部的任何地方,我们还想点击键盘上的 ESC 来关闭弹出框。为了好玩,我们还将添加一个事件,当按下 Enter 时,该事件将继续到下一个 Vue 样式指南规则。

我们可以使用 Vue 事件键修饰符 在负责渲染弹出框内容的组件中处理这个问题。为了使事件能够工作,我们需要确保弹出框在挂载时获得焦点。为此,我们将 tabindex 属性添加到弹出框内容,并添加一个 ref,它将允许我们在挂载钩子中访问元素,并在其上调用 focus 方法。

// VueTourPopoverContent.vue

<template>
  <div
    class="vue-tour-popover-content"
    ref="vueTourPopoverContent"
    tabindex="-1"
    @keydown.enter="proceedToNextStep"
    @keydown.esc="closePopover"
  >
...
</template
...
<script>
export default {
...
  mounted() {
    this.$refs.vueTourPopoverContent.focus();
  }
...
}
</script>

总结

就这样:一个功能齐全的弹出框组件,我们可以在应用程序的任何地方使用它。以下是一些我们在沿途学到的东西

  • 使用弹出框来公开少量信息或功能。请记住,内容将在用户完成操作时消失。
  • 考虑使用弹出框而不是像侧边栏这样的临时视图。弹出框为内容留出更多空间,并且只是临时的。
  • 启用基于弹出框功能的关闭行为。弹出框只有在需要时才可见。如果它允许用户做出选择,则在用户做出决定后立即关闭弹出框。
  • 小心地将弹出框定位在屏幕上。弹出框的箭头应该始终直接指向触发它的元素,并且永远不应该覆盖触发元素。
  • 一次在屏幕上显示一个弹出框。多个弹出框会分散注意力。
  • 注意弹出框的大小。避免将其做得太大,但请记住,正确使用填充可以使事情看起来既美观又整洁。

如果您不想深入研究代码,只需要组件本身,您可以尝试使用 组件的 npm 包版本

希望您会发现这个组件对您的项目有用!