Wiederverwendbare Popovers, die einen kleinen Akzent hinzufügen

Avatar of Mateusz Rybczonek
Mateusz Rybczonek am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Ein Popover ist eine temporäre Ansicht, die über einem Inhalt auf dem Bildschirm erscheint, wenn ein Benutzer auf eine Steuertaste oder in einen definierten Bereich klickt. Zum Beispiel das Klicken auf ein Info-Symbol bei einem bestimmten Listenelement, um die Details des Elements anzuzeigen. Typischerweise enthält ein Popover einen Pfeil, der auf die Stelle zeigt, von der es aufgetaucht ist.

Popovers eignen sich hervorragend für Situationen, in denen wir einen temporären Kontext anzeigen möchten, um die Aufmerksamkeit des Benutzers zu erregen, wenn er mit einem bestimmten Element auf dem Bildschirm interagiert. Sie bieten zusätzliche Informationen und Anweisungen für Benutzer, ohne den Bildschirm zu überladen. Benutzer können sie einfach schließen, indem sie auf dieselbe Weise klicken, wie sie geöffnet wurden, oder außerhalb des Popovers.

Wir werden uns eine Bibliothek namens popper.js ansehen, mit der wir wiederverwendbare Popover-Komponenten im Vue-Framework erstellen können. Popovers sind der perfekte Komponententyp für ein komponentenbasiertes System wie Vue, da sie in sich geschlossene, gekapselte Komponenten sein können, die eigenständig verwaltet werden, aber überall in einer App verwendet werden können.

Tauchen wir ein und legen wir los.

Aber zuerst: Was ist der Unterschied zwischen einem Popover und einem Tooltip?

Hat der Name "Popover" Sie verwirrt? Die Wahrheit ist, dass Popovers sehr ähnlich wie Tooltips sind, ein weiteres gängiges UI-Muster zur Anzeige von zusätzlichem Kontext in einem eingeschlossenen Element. Es gibt jedoch Unterschiede, also lassen Sie uns diese kurz erläutern, damit wir ein klares Verständnis dessen haben, was wir erstellen.

Tooltips Popovers
Tooltips sind dazu gedacht, genau das zu sein, ein Hinweis oder Tipp, was ein Werkzeug oder eine andere Interaktion tut. Sie sollen den Inhalt, über dem sie schweben, verdeutlichen oder bei der Nutzung helfen, nicht zusätzlichen Inhalt hinzufügen. Popovers hingegen können viel ausführlicher sein, sie können eine Kopfzeile und viele Textzeilen im Körper enthalten.
Tooltips sind normalerweise nur beim Überfahren mit der Maus sichtbar. Aus diesem Grund funktioniert ein Tooltip nicht, wenn Sie den Inhalt lesen und gleichzeitig mit anderen Teilen der Seite interagieren möchten. Popovers sind in der Regel abbrechbar, entweder durch einen Klick auf andere Teile der Seite oder durch einen zweiten Klick auf das Popover-Ziel (je nach Implementierung). Aus diesem Grund können Sie ein Popover so einrichten, dass Sie mit anderen Elementen auf der Seite interagieren können, während Sie dessen Inhalt immer noch lesen können.

Popovers sind am besten für größere Bildschirme geeignet und wir werden ihnen höchstwahrscheinlich in folgenden Anwendungsfällen begegnen:

Wenn wir uns diese Anwendungsfälle ansehen, können wir einige Anforderungen ableiten, die ein gutes Popover ausmachen:

  1. Wiederverwendbarkeit: Ein Popover sollte die Übergabe von benutzerdefiniertem Inhalt ermöglichen.
  2. Abbrechbarkeit: Ein Popover sollte durch Klicken außerhalb des Popovers und durch die Escape-Taste abbrechbar sein.
  3. Positionierung: Ein Popover sollte sich neu positionieren, wenn der Bildschirmrand erreicht wird.
  4. Interaktion: Ein Popover sollte die Interaktion mit dem Inhalt im Popover ermöglichen.

Ich habe ein Beispiel erstellt, auf das wir uns während des Prozesses der Erstellung einer Komponente beziehen können.

Demo ansehen

OK, nachdem wir nun ein grundlegendes Verständnis von Popovers und dem, was wir erstellen werden, haben, kommen wir zu den schrittweisen Details für deren Erstellung mit popper.js.

Schritt 1: Erstellen der BasePopover-Komponente

Beginnen wir mit der Erstellung einer Komponente, die für die Initialisierung und Positionierung des Popovers verantwortlich ist. Wir werden diese Komponente BasePopover.vue nennen und im Template der Komponente zwei Elemente rendern:

  • Popover-Inhalt: Dies ist das Element, das für die Darstellung des Inhalts innerhalb des Popovers verantwortlich ist. Vorerst verwenden wir einen Slot, der es uns ermöglicht, den Inhalt von der übergeordneten Komponente zu übergeben, die für das Rendern unseres Popovers verantwortlich ist (Anforderung #1: Wiederverwendbarkeit).
  • Popover-Overlay: Dies ist das Element, das dafür verantwortlich ist, den Inhalt unter dem Popover zu verdecken und den Benutzer von der Interaktion mit den Elementen außerhalb des Popovers abzuhalten. Es ermöglicht uns auch, das Popover bei einem Klick zu schließen (Anforderung #2: Abbrechbarkeit).
// BasePopover.vue
<template>
  <div>
    <div
      ref="basePopoverContent"
      class="base-popover"
    >
      <slot />
    </div>
    <div
      ref="basePopoverOverlay"
      class="base-popover__overlay"
    />
  </div>
</template>

Im Skriptteil der Komponente

  • importieren wir popper.js (die Bibliothek, die sich um die Positionierung des Popovers kümmert), dann
  • empfangen wir die popoverOptions-Props und schließlich
  • setzen wir die initiale popperInstance auf null (da wir anfangs kein Popover haben).

Beschreiben wir, was das popoverOptions-Objekt enthält:

  • popoverReference: Dies ist ein Objekt, in Bezug auf das das Popover positioniert wird (normalerweise das Element, das das Popover auslöst).
  • placement: Dies ist eine Popper.js-Platzierungsoption, die angibt, wo das Popover in Bezug auf das Popover-Referenzelement angezeigt wird (das Ding, an dem es angebracht ist).
  • offset: Dies ist ein Popper.js-Offset-Modifikator, der es uns ermöglicht, die Popover-Position durch Angabe von x- und y-Koordinaten anzupassen.
import Popper from "popper.js"

export default {
  name: "BasePopover",

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

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

Warum brauchen wir das? Die popper.js-Bibliothek ermöglicht es uns, das Element einfach in Bezug auf ein anderes Element zu positionieren. Sie erledigt auch die Magie, wenn das Popover den Bildschirmrand erreicht, und positioniert es neu, damit es immer im Blickfeld des Benutzers ist (Anforderung #3: Positionierung).

Schritt 2: Popper.js initialisieren

Nachdem wir nun ein BasePopover-Komponenten-Skelett haben, fügen wir einige Methoden hinzu, die für die Positionierung und Anzeige des Popovers zuständig sind.

In der Methode initPopper erstellen wir zunächst ein modifiers-Objekt, das zur Erstellung einer Popper-Instanz verwendet wird. Wir setzen die vom übergeordneten Element empfangenen Optionen (placement und offset) in die entsprechenden Felder des modifiers-Objekts. All diese Felder sind optional, weshalb wir ihre Existenz zuerst prüfen müssen.

Dann initialisieren wir eine neue Popper-Instanz, indem wir übergeben:

  • den popoverReference-Knoten (das Element, auf das das Popover zeigt: popoverReference ref)
  • den Popper-Inhaltsknoten (das Element, das den Popover-Inhalt enthält: basePopoverContent ref)
  • das options-Objekt

Wir setzen auch die preventOverflow-Option, um zu verhindern, dass das Popover außerhalb des Viewports positioniert wird. Nach der Initialisierung setzen wir die Popper-Instanz in unsere popperInstance-Daten-Eigenschaft, um zukünftigen Zugriff auf die von popper.js bereitgestellten Methoden und Eigenschaften zu haben.

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"
          }
        }
      }
    )
  }
...
}

Jetzt, da unsere initPopper-Methode bereit ist, brauchen wir einen Ort, um sie aufzurufen. Der beste Ort dafür ist der mounted-Lifecycle-Hook.

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

Wie Sie sehen, rufen wir im mounted-Hook eine weitere Methode auf: die updateOverlayPosition-Methode. Diese Methode ist eine Absicherung, die verwendet wird, um unser Overlay neu zu positionieren, falls wir andere Elemente auf der Seite haben, die absolut positioniert sind (z. B. NavBar, SideBar). Die Methode stellt sicher, dass das Overlay immer den gesamten Bildschirm abdeckt und der Benutzer nicht mit anderen Elementen als dem Popover und dem Overlay selbst interagieren kann.

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

Schritt 3: Popper zerstören

Wir haben unseren Popper initialisiert, aber jetzt brauchen wir eine Möglichkeit, ihn zu entfernen und zu entsorgen, wenn er geschlossen wird. Es besteht kein Bedarf, ihn zu diesem Zeitpunkt im DOM zu haben.

Wir möchten das Popover schließen, wenn wir irgendwo außerhalb davon klicken. Wir können das tun, indem wir einen Klick-Listener zum Overlay hinzufügen, da wir sichergestellt haben, dass das Overlay immer den gesamten Bildschirm unter unserem Popover abdeckt.

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

Erstellen wir eine Methode, die für die Zerstörung des Popovers zuständig ist. In dieser Methode prüfen wir zunächst, ob die popperInstance tatsächlich existiert, und wenn ja, rufen wir die destroy-Methode von Popper auf, die sicherstellt, dass die Popper-Instanz zerstört wird. Danach bereinigen wir unsere popperInstance-Daten-Eigenschaft, indem wir sie auf null setzen, und emittieren ein closePopover-Ereignis, das in der Komponente behandelt wird, die für das Rendern des Popovers zuständig ist.

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

Schritt 4: BasePopover-Komponente rendern

OK, wir haben unser Popover bereit zum Rendern. Dies tun wir in unserer übergeordneten Komponente, die für die Verwaltung der Sichtbarkeit des Popovers und die Übergabe des Inhalts an dieses zuständig ist.

Im Template benötigen wir ein Element, das für das Auslösen unseres Popovers verantwortlich ist (popoverReference), und die BasePopover-Komponente. Die BasePopover-Komponente erhält eine popoverOptions-Eigenschaft, die der Komponente mitteilt, wie wir sie anzeigen möchten, und eine isPopoverVisible-Eigenschaft, die an die v-if-Direktive gebunden ist, die für das Ein- und Ausblenden des Popovers verantwortlich ist.

<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>

Im Skriptteil der Komponente importieren wir unsere BasePopover-Komponente, setzen das isPopoverVisible-Flag initial auf false und das popoverOptions-Objekt, das zur Konfiguration des Popovers beim Initialisieren verwendet wird.

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

Wir setzen die popoverReference-Eigenschaft initial auf null, da das Element, das der Popover-Auslöser sein wird, nicht existiert, wenn unsere übergeordnete Komponente erstellt wird. Dies beheben wir im mounted-Lifecycle-Hook, wenn die Komponente (und die Popover-Referenz) gerendert werden.

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

Nun erstellen wir zwei Methoden, openPopover und closePopover, die für das Ein- und Ausblenden unseres Popovers durch Setzen des richtigen Werts für die isPopoverVisible-Eigenschaft verantwortlich sind.

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

Das Letzte, was wir in diesem Schritt tun müssen, ist, diese Methoden an die entsprechenden Elemente in unserem Template anzuhängen. Wir hängen die openPopover-Methode an das Klickereignis unseres Auslöseelements und die closePopover-Methode an das closePopover-Ereignis an, das von der BasePopover-Komponente emittiert wird, wenn das Popover durch Klicken auf das Popover-Overlay zerstört wird.

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

Mit diesen Vorkehrungen wird unser Popover angezeigt, wenn wir auf das Auslöseelement klicken, und verschwindet, wenn wir außerhalb des Popovers klicken.

Schritt 5: BasePopoverContent-Komponente erstellen

Es sieht jedoch noch nicht wie ein Popover aus. Sicher, es rendert den an die BasePopover-Komponente übergebenen Inhalt, aber das tut es ohne den üblichen Popover-Wrapper und einen Pfeil, der auf das Trigger-Element zeigt. Wir hätten den Wrapper-Komponente in die BasePopover-Komponente aufnehmen können, aber das hätte sie weniger wiederverwendbar gemacht und das Popover an eine bestimmte Template-Implementierung gekoppelt. Unsere Lösung ermöglicht es uns, jedes Template im Popover zu rendern. Wir wollen auch sicherstellen, dass die Komponente nur für die Positionierung und Anzeige des Inhalts verantwortlich ist.

Damit es wie ein Popover aussieht, erstellen wir eine BasePopoverContent-Komponente. Wir müssen zwei Elemente im Template rendern:

  • ein Pfeilelement mit einem x-arrow-Selektor von popper.js, der für popper.js benötigt wird, um den Pfeil richtig zu positionieren.
  • ein Inhalts-Wrapper, der ein Slot-Element bereitstellt, in das unser Inhalt gerendert wird.
<template>
  <div class="base-popover-content">
    <div class="base-popover-content__arrow" x-arrow/>
    <div class="base-popover-content__body">
      <slot/>
    </div>
  </div>
</template>

Nun verwenden wir unsere Wrapper-Komponente in der übergeordneten Komponente, wo wir BasePopover verwenden.

<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>

Und da haben wir es!

Sie können das Popover in dem obigen Beispiel hinein- und hinausanimieren sehen. Wir haben Animationen aus diesem Artikel ausgelassen, um die Kürze zu wahren, aber Sie können sich andere popper.js-Beispiele zur Inspiration ansehen.

Den Animationscode und ein funktionierendes Beispiel finden Sie hier.

Betrachten wir unsere Anforderungen und sehen wir, ob wir etwas übersehen haben.

Bestanden? Anforderung Erklärung
Bestanden Wiederverwendbarkeit Wir haben einen Slot in der BasePopover-Komponente verwendet, der die Implementierung des Popovers von der Inhaltsvorlage entkoppelt. Dies ermöglicht es uns, beliebigen Inhalt an die Komponente zu übergeben.
Fehlgeschlagen Abbrechbarkeit Wir haben es möglich gemacht, das Popover durch Klicken außerhalb zu schließen. Wir müssen immer noch sicherstellen, dass wir das Popover durch Drücken der ESC-Taste auf der Tastatur abbrechen können.
Bestanden Positionierung Da hat uns die popper.js-Bibliothek ein Problem gelöst. Sie hat uns nicht nur Superkräfte bei der Positionierung verliehen, sondern kümmert sich auch um die Neupositionierung des Popovers, wenn es den Bildschirmrand erreicht.
Fehlgeschlagen Interaktion Wir haben ein Popover, das ein- und ausgeblendet wird, aber wir haben noch keine Interaktionen mit dem Popover-Inhalt. Bis jetzt sieht es eher wie ein Tooltip aus und könnte tatsächlich als Tooltip verwendet werden, wenn es um das Ein- und Ausblenden des Elements geht. Tooltips werden normalerweise beim Überfahren mit der Maus angezeigt, daher ist dies die einzige Änderung, die wir vornehmen müssten.

Verdammt, wir haben die Anforderung an die Interaktion nicht erfüllt. Das Hinzufügen von Interaktion ist eine Frage der Erstellung einer Komponente (oder mehrerer Komponenten), die im BasePopoverContent-Slot platziert wird. Im Beispiel habe ich eine sehr einfache Komponente mit einer Kopfzeile und Text erstellt, die einige Vue-Styleguide-Regeln anzeigt. Durch Klicken auf die Schaltflächen können wir mit dem Popover-Inhalt interagieren und die Regeln ändern. Wenn Sie zur letzten Regel gelangen, ändert die Schaltfläche ihren Zweck und dient als Schließen-Schaltfläche für das Popover. Es ähnelt sehr den Willkommensbildschirmen für neue Benutzer, die wir in Apps sehen.

Wir müssen auch die Anforderung der Abbrechbarkeit vollständig erfüllen. Wir möchten die ESC-Taste auf der Tastatur drücken, um das Popover zu schließen, zusätzlich zum Klicken irgendwo außerhalb davon. Zum Spaß fügen wir auch ein Ereignis hinzu, das zur nächsten Vue-Styleguide-Regel wechselt, wenn Enter gedrückt wird.

Wir können dies in der Komponente, die für das Rendern des Popover-Inhalts verantwortlich ist, mithilfe von Vue-Ereignisschlüsselmodifikatoren behandeln. Damit die Ereignisse funktionieren, müssen wir sicherstellen, dass das Popover beim Mounten den Fokus erhält. Dazu fügen wir ein tabindex-Attribut zum Popover-Inhalt und eine ref hinzu, die uns den Zugriff auf das Element im mounted-Hook ermöglicht und die focus-Methode darauf aufruft.

// 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>

Zusammenfassung

Und da haben wir es: eine voll funktionsfähige Popover-Komponente, die wir überall in unserer App verwenden können. Hier sind ein paar Dinge, die wir auf dem Weg gelernt haben:

  • Verwenden Sie ein Popover, um eine kleine Menge an Informationen oder Funktionalität anzuzeigen. Denken Sie daran, dass der Inhalt verschwindet, sobald der Benutzer damit fertig ist.
  • Erwägen Sie die Verwendung von Popovers anstelle von temporären Ansichten wie Sidebars. Popovers lassen mehr Platz für Inhalte und sind nur temporär.
  • Aktivieren Sie ein Schließverhalten, das für die Funktion des Popovers sinnvoll ist. Ein Popover sollte nur sichtbar sein, wenn es benötigt wird. Wenn es dem Benutzer erlaubt, eine Entscheidung zu treffen, schließen Sie das Popover, sobald der Benutzer seine Entscheidung getroffen hat.
  • Positionieren Sie Popovers sorgfältig auf dem Bildschirm. Der Pfeil eines Popovers sollte immer direkt auf das Element zeigen, das es ausgelöst hat, und sollte niemals das Auslöseelement verdecken.
  • Zeigen Sie jeweils nur ein Popover auf dem Bildschirm an. Mehr als eines stiehlt die Aufmerksamkeit.
  • Achten Sie auf die Größe des Popovers. Vermeiden Sie es, es zu groß zu machen, aber bedenken Sie, dass die richtige Verwendung von Abständen Dinge schön und sauber aussehen lassen kann.

Wenn Sie nicht zu tief in den Code eintauchen möchten und die Komponente einfach nur so benötigen, wie sie ist, können Sie die npm-Paketversion der Komponente ausprobieren.

Hoffentlich finden Sie diese Komponente in Ihrem Projekt nützlich!