Skip to content

Popover with Arrow

Solution

ts
import { arrow, autoUpdate, flip, hide, offset, computePosition, shift } from '@vielzeug/floatit';

const trigger = document.querySelector<HTMLElement>('#btn')!;
const popover = document.querySelector<HTMLElement>('#popover')!;
const arrowEl = popover.querySelector<HTMLElement>('.arrow')!;

let cleanup: (() => void) | null = null;

function update() {
  const result = computePosition(trigger, popover, {
    placement: 'top',
    middleware: [offset(12), flip(), shift({ padding: 8 }), arrow({ element: arrowEl, padding: 8 }), hide()],
  });

  popover.style.left = `${result.x}px`;
  popover.style.top = `${result.y}px`;

  popover.dataset.placement = result.placement;

  const arrowData = result.middlewareData.arrow as { x?: number; y?: number } | undefined;
  arrowEl.style.left = arrowData?.x != null ? `${arrowData.x}px` : '';
  arrowEl.style.top = arrowData?.y != null ? `${arrowData.y}px` : '';

  const hideData = result.middlewareData.hide as { escaped?: boolean; referenceHidden?: boolean } | undefined;
  popover.style.visibility = hideData?.referenceHidden ? 'hidden' : 'visible';
}

trigger.addEventListener('click', () => {
  if (popover.hasAttribute('data-open')) {
    popover.removeAttribute('data-open');
    cleanup?.();
    cleanup = null;
    return;
  }

  popover.setAttribute('data-open', '');
  cleanup = autoUpdate(trigger, popover, update);
});
css
#popover .arrow {
  position: absolute;
}

#popover[data-placement^='top'] .arrow {
  bottom: -5px;
}

#popover[data-placement^='bottom'] .arrow {
  top: -5px;
}

#popover[data-placement^='left'] .arrow {
  right: -5px;
}

#popover[data-placement^='right'] .arrow {
  left: -5px;
}