JSFiddle - React, Tailwind, and code Playground

by elclanrs

HTML

<p>
  <label for="position">Position:</label>

  <select name="position" id="position">
    <option value="top">Top</option>
    <option value="right">Right</option>
    <option value="bottom">Bottom</option>
    <option value="left">Left</option>
  </select>
</p>

<div class="popover-container">
  <button id="trigger" data-popover-target="my-popover">Popover</button>

  <template data-popover="my-popover">
    This is the popover content!
  </template>
</div>

SCSS

body {
  font: 16px/1.4 normal Arial, sans-serif;
}

.popover-container {
  margin: 300px;
}

@keyframes slide-top {
  0% {
    opacity: 0;
    transform: translateY(-15%);
  }

  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slide-right {
  0% {
    opacity: 0;
    transform: translateX(15%);
  }

  100% {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes slide-bottom {
  0% {
    opacity: 0;
    transform: translateY(15%);
  }

  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slide-left {
  0% {
    opacity: 0;
    transform: translateX(-15%);
  }

  100% {
    opacity: 1;
    transform: translateX(0);
  }
}

.popover {
  $pad: 1.5em;
  $bg-color: white;
  $border-color: #aaa;
  $arrow-pad: 8px;
  $arrow-size: 8px;
  $radius: 4px;

  padding: $pad;
  border: 1px solid $border-color;
  border-radius: $radius;
  background: $bg-color;
  box-shadow: 0 1px 4px rgba(0,0,0,.2);

  &--top {
    margin-top: -$arrow-size - $arrow-pad;
    animation: .4s slide-top;

    &::before, &::after {
      content: "";
      position: absolute;
      top: 100%;
      left: 50%;
      margin-left: -$arrow-size;
      border: $arrow-size solid transparent;
      border-top-color: $bg-color;
    }

    &::before {
      margin-top: 1px;
      border-top-color: darken($border-color, 25%);
    }
  }

  &--right {
    margin-left: $arrow-size + $arrow-pad;
    animation: .4s slide-right;

    &::before, &::after {
      content: "";
      position: absolute;
      top: 50%;
      right: 100%;
      margin-top: -$arrow-size;
      border: $arrow-size solid transparent;
      border-right-color: $bg-color;
    }

    &::before {
      margin-right: 1px;
      border-right-color: darken($border-color, 25%);
    }
  }

  &--bottom {
    margin-top: $arrow-size + $arrow-pad;
    animation: .4s slide-bottom;

    &::before, &::after {
      content: "";
      position: absolute;
      bottom: 100%;
      left: 50%;
     ...

JavaScript

function isInViewport(element) {
  const rect = element.getBoundingClientRect();
  const html = document.documentElement;
  return rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || html.clientHeight) &&
    rect.right <= (window.innerWidth || html.clientWidth);
}

class Popover {
  constructor(trigger, { position = 'top', className = 'popover' }) {
    this.trigger = trigger;
    this.position = position;
    this.className = className;
    this.orderedPositions = ['top', 'right', 'bottom', 'left'];

    const popoverTemplate = document.querySelector(`[data-popover=${trigger.dataset.popoverTarget}]`);
    this.popover = document.createElement('div');
    this.popover.innerHTML = popoverTemplate.innerHTML;

    Object.assign(this.popover.style, {
      position: 'fixed'
    });

    this.popover.classList.add(className);

    this.handleWindowEvent = () => {
      if (this.isVisible) {
        this.show();
      }
    };

    this.handleDocumentEvent = (evt) => {
      if (this.isVisible && evt.target !== this.trigger && evt.target !== this.popover) {
        this.popover.remove();
      }
    };
  }

  get isVisible() {
    return document.body.contains(this.popover);
  }

  show() {
    document.addEventListener('click', this.handleDocumentEvent);
    window.addEventListener('scroll', this.handleWindowEvent);
    window.addEventListener('resize', this.handleWindowEvent);

    document.body.appendChild(this.popover);

    const { top: triggerTop, left: triggerLeft } = this.trigger.getBoundingClientRect();
    const { offsetHeight: triggerHeight, offsetWidth: triggerWidth } = this.trigger;
    const { offsetHeight: popoverHeight, offsetWidth: popoverWidth } = this.popover;

    const positionIndex = this.orderedPositions.indexOf(this.position);

    const positions = {
      top: {
        name: 'top',
        top: triggerTop - popoverHeight,
        left: triggerLeft - ((popoverWidth - triggerWidth) / 2)
      },
      right: {
    ...