text: /**
   * Copyright IBM Corp. 2016, 2018
   *
   * This source code is licensed under the Apache-2.0 license found in the
   * LICENSE file in the root directory of this source tree.
   */
  
  import mixin from '../../globals/js/misc/mixin';
  import createComponent from '../../globals/js/mixins/create-component';
  import eventedShowHideState from '../../globals/js/mixins/evented-show-hide-state';
  import trackBlur from '../../globals/js/mixins/track-blur';
  import getLaunchingDetails from '../../globals/js/misc/get-launching-details';
  import optimizedResize from '../../globals/js/misc/resize';
  
  /**
   * The structure for the position of floating menu.
   * @typedef {Object} FloatingMenu~position
   * @property {number} left The left position.
   * @property {number} top The top position.
   * @property {number} right The right position.
   * @property {number} bottom The bottom position.
   */
  
  /**
   * The structure for the size of floating menu.
   * @typedef {Object} FloatingMenu~size
   * @property {number} width The width.
   * @property {number} height The height.
   */
  
  /**
   * The structure for the position offset of floating menu.
   * @typedef {Object} FloatingMenu~offset
   * @property {number} top The top position.
   * @property {number} left The left position.
   */
  
  export const DIRECTION_LEFT = 'left';
  export const DIRECTION_TOP = 'top';
  export const DIRECTION_RIGHT = 'right';
  export const DIRECTION_BOTTOM = 'bottom';
  
  /**
   * @param {Object} params The parameters.
   * @param {FloatingMenu~size} params.menuSize The size of the menu.
   * @param {FloatingMenu~position} params.refPosition The position of the triggering element.
   * @param {FloatingMenu~offset} [params.offset={ left: 0, top: 0 }] The position offset of the menu.
   * @param {string} [params.direction=bottom] The menu direction.
   * @param {number} [params.scrollX=0] The scroll position of the viewport.
   * @param {number} [params.scrollY=0] The scroll position of the viewport.
   * @returns {FloatingMenu~offset} The position of the menu, relative to the top-left corner of the viewport.
   * @private
   */
  export const getFloatingPosition = ({
    menuSize,
    refPosition,
    offset = {},
    direction = DIRECTION_BOTTOM,
    scrollX = 0,
    scrollY = 0,
  }) => {
    const { left: refLeft = 0, top: refTop = 0, right: refRight = 0, bottom: refBottom = 0 } = refPosition;
  
    const { width, height } = menuSize;
    const { top = 0, left = 0 } = offset;
    const refCenterHorizontal = (refLeft + refRight) / 2;
    const refCenterVertical = (refTop + refBottom) / 2;
  
    return {
      [DIRECTION_LEFT]: {
        left: refLeft - width + scrollX - left,
        top: refCenterVertical - height / 2 + scrollY + top,
      },
      [DIRECTION_TOP]: {
        left: refCenterHorizontal - width / 2 + scrollX + left,
        top: refTop - height + scrollY - top,
      },
      [DIRECTION_RIGHT]: {
        left: refRight + scrollX + left,
        top: refCenterVertical - height / 2 + scrollY + top,
      },
      [DIRECTION_BOTTOM]: {
        left: refCenterHorizontal - width / 2 + scrollX + left,
        top: refBottom + scrollY + top,
      },
    }[direction];
  };
  
  class FloatingMenu extends mixin(createComponent, eventedShowHideState, trackBlur) {
    /**
     * Floating menu.
     * @extends CreateComponent
     * @extends EventedShowHideState
     * @param {HTMLElement} element The element working as a modal dialog.
     * @param {Object} [options] The component options.
     * @param {string} [options.selectorContainer] The CSS selector to find the container to put this menu in.
     * @param {string} [options.attribDirection] The attribute name to specify menu placement direction (top/right/bottom/left).
     * @param {string} [options.classShown] The CSS class for shown state, for the menu.
     * @param {string} [options.classRefShown] The CSS class for shown state, for the trigger button.
     * @param {string} [options.eventBeforeShown]
     *   The name of the custom event fired before this menu is shown.
     *   Cancellation of this event stops hiding the menu.
     * @param {string} [options.eventAfterShown]
     *   The name of the custom event telling that menu is sure shown
     *   without being canceled by the event handler named by `eventBeforeShown` option (`floating-menu-beingshown`).
     * @param {string} [options.eventBeforeHidden]
     *   The name of the custom event fired before this menu is hidden.
     *   Cancellation of this event stops hiding the menu.
     * @param {string} [options.eventAfterHidden]
     *   The name of the custom event telling that menu is sure hidden
     *   without being canceled by the event handler named by `eventBeforeHidden` option (`floating-menu-beinghidden`).
     * @param {Element} [options.refNode] The launching element of the menu. Used for calculating the geometry of the menu.
     * @param {Object} [options.offset] The offset to adjust the geometry of the menu. Should have `top`/`left` properties.
     */
    constructor(element, options) {
      super(element, options);
      const attribDirectionValue = this.element.getAttribute(this.options.attribDirection);
      if (!this.options.direction) {
        this.options.direction = attribDirectionValue || 'bottom';
      }
      if (!attribDirectionValue) {
        // Update attribute for styling
        this.element.setAttribute(this.options.attribDirection, this.options.direction);
      }
    }
  
    /**
     * Focuses back on the trigger button if this component loses focus.
     */
    handleBlur(event) {
      if (this.element.classList.contains(this.options.classShown)) {
        this.changeState('hidden', getLaunchingDetails(event));
        const { refNode } = this.options;
        if (this.element.contains(event.relatedTarget) && refNode && event.target !== refNode) {
          HTMLElement.prototype.focus.call(refNode); // SVGElement in IE11 does not have `.focus()` method
        }
      }
    }
  
    /**
     * @private
     * @returns {Element} The element that this menu should be placed to.
     */
    _getContainer() {
      return this.element.closest(this.options.selectorContainer) || this.element.ownerDocument.body;
    }
  
    /**
     * @private
     * @returns {Object} The menu position, with `top` and `left` properties.
     */
    _getPos() {
      const { element } = this;
      const { refNode, offset, direction } = this.options;
  
      if (!refNode) {
        throw new Error('Cannot find the refernce node for positioning floating menu.');
      }
  
      return getFloatingPosition({
        menuSize: element.getBoundingClientRect(),
        refPosition: refNode.getBoundingClientRect(),
        offset: typeof offset !== 'function' ? offset : offset(element, direction, refNode),
        direction,
        scrollX: refNode.ownerDocument.defaultView.pageXOffset,
        scrollY: refNode.ownerDocument.defaultView.pageYOffset,
      });
    }
  
    /**
     * Sees if the computed style is what this floating menu expects.
     * @private
     */
    _testStyles() {
      if (!this.options.debugStyle) {
        return;
      }
      const { element } = this;
      const computedStyle = element.ownerDocument.defaultView.getComputedStyle(element);
      const styles = {
        position: 'absolute',
        right: 'auto',
        margin: 0,
      };
      Object.keys(styles).forEach(key => {
        const expected = typeof styles[key] === 'number' ? parseFloat(styles[key]) : styles[key];
        const actual = computedStyle.getPropertyValue(key);
        if (expected !== actual) {
          // eslint-disable-next-line no-console
          console.warn(`Floating menu component expects ${key}: ${styles[key]} style.`);
        }
      });
    }
  
    /**
     * Places the menu.
     * @private
     */
    _place() {
      const { element } = this;
      const { left, top } = this._getPos();
      element.style.left = `${left}px`;
      element.style.top = `${top}px`;
      this._testStyles();
    }
  
    /**
     * @param {string} state The new state.
     * @returns {boolean} `true` of the current state is different from the given new state.
     */
    shouldStateBeChanged(state) {
      return (
        (state === 'shown' || state === 'hidden') &&
        state !== (this.element.classList.contains(this.options.classShown) ? 'shown' : 'hidden')
      );
    }
  
    /**
     * Changes the shown/hidden state.
     * @private
     * @param {string} state The new state.
     * @param {Object} detail The detail of the event trigging this action.
     * @param {Function} callback Callback called when change in state completes.
     */
    _changeState(state, detail, callback) {
      const shown = state === 'shown';
      const { refNode, classShown, classRefShown } = this.options;
      if (!refNode) {
        throw new TypeError('Cannot find the refernce node for changing the style.');
      }
      this.element.classList.toggle(classShown, shown);
      if (classRefShown) {
        refNode.classList.toggle(classRefShown, shown);
      }
      if (state === 'shown') {
        if (!this.hResize) {
          this.hResize = optimizedResize.add(() => {
            this._place();
          });
        }
        this._getContainer().appendChild(this.element);
        this._place();
        // IE11 puts focus on elements with `.focus()`, even ones without `tabindex` attribute
        if (!this.element.hasAttribute(this.options.attribAvoidFocusOnOpen)) {
          (this.element.querySelector(this.options.selectorPrimaryFocus) || this.element).focus();
        }
      }
      if (state === 'hidden' && this.hResize) {
        this.hResize.release();
        this.hResize = null;
      }
      callback();
    }
  
    release() {
      if (this.hResize) {
        this.hResize.release();
        this.hResize = null;
      }
      super.release();
    }
  
    static options /* #__PURE_CLASS_PROPERTY__ */ = {
      selectorContainer: '[data-floating-menu-container]',
      selectorPrimaryFocus: '[data-floating-menu-primary-focus]',
      attribDirection: 'data-floating-menu-direction',
      attribAvoidFocusOnOpen: 'data-avoid-focus-on-open',
      classShown: '', // Should be provided from options arg in constructor
      classRefShown: '', // Should be provided from options arg in constructor
      eventBeforeShown: 'floating-menu-beingshown',
      eventAfterShown: 'floating-menu-shown',
      eventBeforeHidden: 'floating-menu-beinghidden',
      eventAfterHidden: 'floating-menu-hidden',
      refNode: null, // Should be provided from options arg in constructor
      offset: {
        left: 0,
        top: 0,
      },
    };
  
    static components /* #__PURE_CLASS_PROPERTY__ */ = new WeakMap();
  }
  
  export default FloatingMenu;
  