@hashicorp/design-system-components
Version: 
Helios Design System Components
190 lines (172 loc) • 8.64 kB
JavaScript
import { modifier } from 'ember-modifier';
import { assert } from '@ember/debug';
import { autoUpdate, limitShift, offset, flip, shift, autoPlacement, arrow, size, computePosition } from '@floating-ui/dom';
/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: MPL-2.0
 */
let HdsEnableCollisionDetectionOptions = /*#__PURE__*/function (HdsEnableCollisionDetectionOptions) {
  HdsEnableCollisionDetectionOptions["Shift"] = "shift";
  HdsEnableCollisionDetectionOptions["Flip"] = "flip";
  HdsEnableCollisionDetectionOptions["Auto"] = "auto";
  return HdsEnableCollisionDetectionOptions;
}({});
const DEFAULT_PLACEMENT = 'bottom';
const PLACEMENTS = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end'];
const ENABLE_COLLISION_DETECTION_OPTIONS = Object.values(HdsEnableCollisionDetectionOptions);
// share the same default value of "padding" for `flip/shift/autoPlacement` options
// this refers to the minimum distance from the boundaries' edges (the viewport)
// before the floating element changes its position (flips, shifts, or autoplace itself)
const DEFAULT_EDGE_DISTANCE = 8;
// we use this function to process all the options provided to the modifier in a single place,
// in relation to the Floating UI APIs, and keep the modifier code more clean/simple
const getFloatingUIOptions = options => {
  const {
    placement = DEFAULT_PLACEMENT,
    strategy = 'absolute',
    // we don't need to use `fixed` if we use the Popover API for the "floating" element (it puts the element in the `top-layer`)
    offsetOptions,
    flipOptions = {
      padding: DEFAULT_EDGE_DISTANCE
    },
    shiftOptions = {
      padding: DEFAULT_EDGE_DISTANCE,
      limiter: limitShift()
    },
    autoPlacementOptions = {
      padding: DEFAULT_EDGE_DISTANCE
    },
    middlewareExtra = [],
    enableCollisionDetection,
    arrowElement,
    arrowPadding,
    matchToggleWidth
  } = options;
  // we build dynamically the list of middleware functions to invoke, depending on the options provided
  const middleware = [];
  // https://floating-ui.com/docs/offset
  middleware.push(offset(offsetOptions));
  // https://floating-ui.com/docs/flip
  // https://floating-ui.com/docs/shift
  // https://floating-ui.com/docs/autoPlacement
  if (enableCollisionDetection === true || enableCollisionDetection === 'flip') {
    middleware.push(flip(flipOptions));
  }
  if (enableCollisionDetection === true || enableCollisionDetection === 'shift') {
    middleware.push(shift(shiftOptions));
  }
  if (enableCollisionDetection === 'auto') {
    middleware.push(autoPlacement(autoPlacementOptions));
  }
  // https://floating-ui.com/docs/arrow
  if (arrowElement) {
    middleware.push(arrow({
      element: arrowElement,
      padding: arrowPadding ?? 0
    }));
  }
  // https://floating-ui.com/docs/size#match-reference-width
  if (matchToggleWidth) {
    middleware.push(size({
      apply({
        rects,
        elements
      }) {
        // wrap Object.assign inside a requestAnimationFrame to avoid ResizeObserver loop limit exceeded error
        // https://github.com/floating-ui/floating-ui/issues/1740
        requestAnimationFrame(() => {
          Object.assign(elements.floating.style, {
            width: `${rects.reference.width}px`
          });
        });
      }
    }));
  }
  middleware.push(...middlewareExtra);
  return {
    placement,
    strategy,
    middleware
  };
};
// Notice: we use a function-based modifier here instead of a class-based one
// because it's quite simple in its logic, and doesn't require injecting services
// see: https://github.com/ember-modifier/ember-modifier#function-based-modifiers
var anchoredPositionModifier = modifier((element, positional, named = {}) => {
  // the element that "floats" next to the "anchor" (whose position is calculated in relation to the anchor)
  // notice: this is the element the Ember modifier is attached to
  const _floatingElement = element;
  // the element that acts as an "anchor" for the "floating" element
  // it can be a DOM (string) selector or a DOM element
  // notice: it's expressed as "positional" argument (array of arguments) for the modifier
  const _anchorTarget = positional[0];
  const _anchorElement = typeof _anchorTarget === 'string' ? document.querySelector(_anchorTarget) : _anchorTarget;
  assert('`hds-anchored-position` modifier - the provided "anchoring" element is not defined correctly', _anchorElement instanceof HTMLElement || _anchorElement instanceof SVGElement);
  // the "arrow" element (optional) associated with the "floating" element
  // it can be a DOM selector (string) or a DOM element
  // notice: it's declared inside the "named" argument (object) for the modifier
  // but we need to extract it also here so it can be used to assign inline styles to it
  let arrowElement;
  if (named.arrowElement) {
    assert('`hds-anchored-position` modifier - the `element` provided for the "arrow" element is not a valid DOM node', named.arrowElement instanceof HTMLElement || named.arrowElement instanceof SVGElement);
    arrowElement = named.arrowElement;
  } else if (named.arrowSelector) {
    assert('`hds-anchored-position` modifier - the `selector` provided for the "arrow" element must be a string', typeof named.arrowSelector === 'string');
    const selectedArrowElement = document.querySelector(named.arrowSelector);
    if (selectedArrowElement instanceof HTMLElement) {
      arrowElement = selectedArrowElement;
    } else {
      assert('`hds-anchored-position` modifier - the `selector` provided for the "arrow" element is not a valid DOM selector');
    }
  }
  // the Floating UI "options" to apply to the "floating" element
  // notice: we spread the `named` argument and override its `arrowElement` value instead of setting it directly because Ember complains that modifier's arguments must be immutable
  const floatingOptions = getFloatingUIOptions({
    ...named,
    arrowElement
  });
  const computeFloatingPosition = async () => {
    // important to know: `computePosition()` is not stateful, it only positions the "floating" element once
    // see: https://floating-ui.com/docs/computePosition
    const state = await computePosition(_anchorElement, _floatingElement, floatingOptions);
    const {
      x,
      y,
      placement,
      strategy,
      middlewareData
    } = state;
    Object.assign(_floatingElement.style, {
      position: strategy,
      top: `${y}px`,
      left: `${x}px`
      // TODO? commenting this for now, will need to make this conditional to some argument (and understand how this relates to the `@height` argument)
      // maxHeight: `${middlewareData.size.availableHeight - 10}px`,
    });
    if (arrowElement && middlewareData.arrow) {
      // we assign a "data" attribute to the "arrow" element so we can use CSS (in the consuming components) to position/rotate it accordingly and we avoid calculating at runtime values that technically we already know
      // (similar to what Tippy.js does: https://github.com/atomiks/tippyjs/blob/master/src/scss/svg-arrow.scss)
      // IMPORTANT: floating-ui assumes the "arrow" container is square!
      arrowElement.setAttribute('data-hds-anchored-arrow-placement', placement);
      // we set `x` or `y` value (depends on the position of the arrow in relation to the "floating" element placement)
      // see: https://floating-ui.com/docs/arrow#usage
      Object.assign(arrowElement.style, {
        left: middlewareData.arrow.x != null ? `${middlewareData.arrow.x}px` : '',
        top: middlewareData.arrow.y != null ? `${middlewareData.arrow.y}px` : ''
      });
    }
  };
  // the `autoUpdate` function automatically updates the position of the floating element when necessary.
  // it should only be called when the floating element is mounted on the DOM or visible on the screen.
  // it returns a "cleanup" function that should be invoked when the floating element is removed from the DOM or hidden from the screen.
  // see: https://floating-ui.com/docs/autoUpdate
  const cleanupFloatingUI = autoUpdate(_anchorElement, _floatingElement,
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
  computeFloatingPosition);
  // this (teardown) function is run when the element is removed from the DOM
  return () => {
    cleanupFloatingUI();
  };
});
export { DEFAULT_PLACEMENT, ENABLE_COLLISION_DETECTION_OPTIONS, HdsEnableCollisionDetectionOptions, PLACEMENTS, anchoredPositionModifier as default, getFloatingUIOptions };
//# sourceMappingURL=hds-anchored-position.js.map