'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var React = require('react');
var iconsReact = require('@carbon/icons-react');
var classnames = require('classnames');
var invariant = require('../../../../../invariant/invariant.js');
var PropTypes = require('prop-types');
var FloatingMenu = require('../../internal/FloatingMenu.js');
var keys = require('../../internal/keyboard/keys.js');
var match = require('../../internal/keyboard/match.js');
var noopFn = require('../../internal/noopFn.js');
var usePrefix = require('../../internal/usePrefix.js');
var deprecate = require('../../prop-types/deprecate.js');
var mergeRefs = require('../../tools/mergeRefs.js');
var setupGetInstanceId = require('../../tools/setupGetInstanceId.js');
var index = require('../IconButton/index.js');
var useOutsideClick = require('../../internal/useOutsideClick.js');
var deprecateValuesWithin = require('../../prop-types/deprecateValuesWithin.js');
var mapPopoverAlign = require('../../tools/mapPopoverAlign.js');

function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }

var React__default = /*#__PURE__*/_interopDefault(React);
var classnames__default = /*#__PURE__*/_interopDefault(classnames);
var PropTypes__default = /*#__PURE__*/_interopDefault(PropTypes);

/**
 * Copyright IBM Corp. 2016, 2023
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */


const getInstanceId = setupGetInstanceId.setupGetInstanceId();
const on = (target, ...args) => {
  target.addEventListener(...args);
  return {
    release() {
      target.removeEventListener(...args);
      return null;
    }
  };
};

/**
 * The CSS property names of the arrow keyed by the floating menu direction.
 */
const triggerButtonPositionProps = {
  [FloatingMenu.DIRECTION_TOP]: 'bottom',
  [FloatingMenu.DIRECTION_BOTTOM]: 'top'
};

/**
 * Determines how the position of the arrow should affect the floating menu
 * position.
 */
const triggerButtonPositionFactors = {
  [FloatingMenu.DIRECTION_TOP]: -2,
  [FloatingMenu.DIRECTION_BOTTOM]: -1
};

/**
 * Calculates the offset for the floating menu.
 *
 * @param menuBody - The menu body with the menu arrow.
 * @param direction - The floating menu direction.
 * @returns The adjustment of the floating menu position, upon the position of
 *          the menu arrow.
 */
const getMenuOffset = (menuBody, direction, trigger, flip) => {
  const triggerButtonPositionProp = triggerButtonPositionProps[direction];
  const triggerButtonPositionFactor = triggerButtonPositionFactors[direction];
  {
    !(triggerButtonPositionProp && triggerButtonPositionFactor) ? invariant(false, '[OverflowMenu] wrong floating menu direction: `%s`', direction)  : void 0;
  }
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- https://github.com/carbon-design-system/carbon/issues/20452
  const {
    offsetWidth: menuWidth,
    offsetHeight: menuHeight
  } = menuBody;
  switch (triggerButtonPositionProp) {
    case 'top':
    case 'bottom':
      {
        // TODO: Ensure `trigger` is there for `<OverflowMenu open>`
        const triggerWidth = !trigger ? 0 : trigger.offsetWidth;
        return {
          left: (!flip ? 1 : -1) * (menuWidth / 2 - triggerWidth / 2),
          top: 0
        };
      }
    default:
      return {
        left: 0,
        top: 0
      };
  }
};
// eslint-disable-next-line react/display-name -- https://github.com/carbon-design-system/carbon/issues/20452
const OverflowMenu = /*#__PURE__*/React.forwardRef(({
  align,
  ['aria-label']: ariaLabel = null,
  ariaLabel: deprecatedAriaLabel,
  children,
  className,
  direction = FloatingMenu.DIRECTION_BOTTOM,
  flipped = false,
  focusTrap = true,
  iconClass,
  iconDescription = 'Options',
  id,
  light,
  menuOffset = getMenuOffset,
  menuOffsetFlip = getMenuOffset,
  menuOptionsClass,
  onClick = noopFn.noopFn,
  onClose = noopFn.noopFn,
  onOpen = noopFn.noopFn,
  open: openProp,
  renderIcon: IconElement = iconsReact.OverflowMenuVertical,
  selectorPrimaryFocus = '[data-floating-menu-primary-focus]',
  size = 'md',
  innerRef,
  ...other
}, ref) => {
  const prefix = React.useContext(usePrefix.PrefixContext);
  const [open, setOpen] = React.useState(openProp ?? false);
  const [click, setClick] = React.useState(false);
  const [hasMountedTrigger, setHasMountedTrigger] = React.useState(false);
  /**  The handle of `onfocusin` or `focus` event handler. */
  const hFocusIn = React.useRef(null);
  const instanceId = React.useRef(getInstanceId());
  const menuBodyRef = React.useRef(null);
  const menuItemRefs = React.useRef({});
  const prevOpenProp = React.useRef(openProp);
  const prevOpenState = React.useRef(open);
  /** The element ref of the tooltip's trigger button. */
  const triggerRef = React.useRef(null);
  const wrapperRef = React.useRef(null);

  // Sync open prop changes.
  React.useEffect(() => {
    if (prevOpenProp.current !== openProp) {
      setOpen(!!openProp);
      prevOpenProp.current = openProp;
    }
  }, [openProp]);

  // Mark trigger as mounted.
  React.useEffect(() => {
    if (triggerRef.current) {
      setHasMountedTrigger(true);
    }
  }, []);
  React.useEffect(() => {
    if (open && !prevOpenState.current) {
      onOpen();
    } else if (!open && prevOpenState.current) {
      onClose();
    }
    prevOpenState.current = open;
  }, [open, onClose, onOpen]);
  useOutsideClick.useOutsideClick(wrapperRef, ({
    target
  }) => {
    if (open && (!menuBodyRef.current || target instanceof Node && !menuBodyRef.current.contains(target))) {
      closeMenu();
    }
  });
  const focusMenuEl = React.useCallback(() => {
    if (triggerRef.current) {
      triggerRef.current.focus();
    }
  }, []);
  const closeMenu = React.useCallback(onCloseMenu => {
    setOpen(false);
    // Optional callback to be executed after the state as been set to close
    if (onCloseMenu) {
      onCloseMenu();
    }
  }, []);
  const closeMenuAndFocus = React.useCallback(() => {
    const wasClicked = click;
    const wasOpen = open;
    closeMenu(() => {
      if (wasOpen && !wasClicked) {
        focusMenuEl();
      }
    });
  }, [click, open, closeMenu, focusMenuEl]);
  const closeMenuOnEscape = React.useCallback(() => {
    const wasOpen = open;
    closeMenu(() => {
      if (wasOpen) {
        focusMenuEl();
      }
    });
  }, [open, closeMenu, focusMenuEl]);
  const handleClick = evt => {
    setClick(true);
    if (!menuBodyRef.current || !menuBodyRef.current.contains(evt.target)) {
      setOpen(prev => !prev);
      onClick(evt);
    }
  };
  const handleKeyPress = evt => {
    if (open && match.matches(evt, [keys.ArrowUp, keys.ArrowRight, keys.ArrowDown, keys.ArrowLeft])) {
      evt.preventDefault();
    }

    // Close the overflow menu on escape
    if (match.matches(evt, [keys.Escape])) {
      closeMenuOnEscape();

      // Stop the esc keypress from bubbling out and closing something it shouldn't
      evt.stopPropagation();
    }
  };

  /**
   * Focuses the next enabled overflow menu item given the currently focused
   * item index and direction to move.
   */
  const handleOverflowMenuItemFocus = ({
    currentIndex = 0,
    direction
  }) => {
    const enabledIndices = React.Children.toArray(children).reduce((acc, curr, i) => {
      if (/*#__PURE__*/React__default.default.isValidElement(curr) && !curr.props.disabled) {
        acc.push(i);
      }
      return acc;
    }, []);
    const nextValidIndex = (() => {
      const nextIndex = enabledIndices.indexOf(currentIndex) + direction;
      switch (nextIndex) {
        case -1:
          return enabledIndices.length - 1;
        case enabledIndices.length:
          return 0;
        default:
          return nextIndex;
      }
    })();
    const overflowMenuItem = menuItemRefs.current[enabledIndices[nextValidIndex]];
    overflowMenuItem?.focus();
  };
  const bindMenuBody = menuBody => {
    if (!menuBody) {
      menuBodyRef.current = menuBody;
    }
    if (!menuBody && hFocusIn.current) {
      hFocusIn.current = hFocusIn.current.release();
    }
  };
  const handlePlace = menuBody => {
    if (!menuBody) return;
    menuBodyRef.current = menuBody;
    const hasFocusin = 'onfocusin' in window;
    const focusinEventName = hasFocusin ? 'focusin' : 'focus';
    hFocusIn.current = on(menuBody.ownerDocument, focusinEventName, event => {
      const target = event.target;
      const triggerEl = triggerRef.current;
      if (typeof target.matches === 'function') {
        if (!menuBody.contains(target) && triggerEl && !target.matches(`.${prefix}--overflow-menu:first-child, .${prefix}--overflow-menu-options:first-child`)) {
          closeMenuAndFocus();
        }
      }
    }, !hasFocusin);
  };
  const getTarget = () => {
    const triggerEl = triggerRef.current;
    if (triggerEl instanceof Element) {
      return triggerEl.closest('[data-floating-menu-container]') || document.body;
    }
    return document.body;
  };
  const menuBodyId = `overflow-menu-${instanceId.current}__menu-body`;
  const overflowMenuClasses = classnames__default.default(className, `${prefix}--overflow-menu`, {
    [`${prefix}--overflow-menu--open`]: open,
    [`${prefix}--overflow-menu--light`]: light,
    [`${prefix}--overflow-menu--${size}`]: size
  });
  const overflowMenuOptionsClasses = classnames__default.default(menuOptionsClass, `${prefix}--overflow-menu-options`, {
    [`${prefix}--overflow-menu--flip`]: flipped,
    [`${prefix}--overflow-menu-options--open`]: open,
    [`${prefix}--overflow-menu-options--light`]: light,
    [`${prefix}--overflow-menu-options--${size}`]: size
  });
  const overflowMenuIconClasses = classnames__default.default(`${prefix}--overflow-menu__icon`, iconClass);
  const childrenWithProps = React.Children.toArray(children).map((child, index) => {
    if (/*#__PURE__*/React.isValidElement(child)) {
      const childElement = child;
      return /*#__PURE__*/React.cloneElement(childElement, {
        closeMenu: childElement.props.closeMenu || closeMenuAndFocus,
        handleOverflowMenuItemFocus,
        ref: el => {
          menuItemRefs.current[index] = el;
        },
        index
      });
    }
    return null;
  });
  const menuBody = /*#__PURE__*/React__default.default.createElement("ul", {
    className: overflowMenuOptionsClasses,
    tabIndex: -1,
    role: "menu",
    "aria-label": ariaLabel || deprecatedAriaLabel,
    onKeyDown: handleKeyPress,
    id: menuBodyId
  }, childrenWithProps);
  const wrappedMenuBody = /*#__PURE__*/React__default.default.createElement(FloatingMenu.FloatingMenu, {
    focusTrap: focusTrap,
    triggerRef: triggerRef,
    menuDirection: direction,
    menuOffset: flipped ? menuOffsetFlip : menuOffset,
    menuRef: bindMenuBody,
    flipped: flipped,
    target: getTarget,
    onPlace: handlePlace,
    selectorPrimaryFocus: selectorPrimaryFocus
  }, /*#__PURE__*/React.cloneElement(menuBody, {
    'data-floating-menu-direction': direction
  }));
  const combinedRef = innerRef ? mergeRefs.mergeRefs(triggerRef, innerRef, ref) : mergeRefs.mergeRefs(triggerRef, ref);
  return /*#__PURE__*/React__default.default.createElement(React__default.default.Fragment, null, /*#__PURE__*/React__default.default.createElement("span", {
    className: `${prefix}--overflow-menu__wrapper`,
    "aria-owns": open ? menuBodyId : undefined,
    ref: wrapperRef
  }, /*#__PURE__*/React__default.default.createElement(index.IconButton, _rollupPluginBabelHelpers.extends({}, other, {
    align: align,
    type: "button",
    "aria-haspopup": true,
    "aria-expanded": open,
    "aria-controls": open ? menuBodyId : undefined,
    className: overflowMenuClasses,
    onClick: handleClick,
    id: id,
    ref: combinedRef,
    size: size,
    label: iconDescription,
    kind: "ghost"
  }), /*#__PURE__*/React__default.default.createElement(IconElement, {
    className: overflowMenuIconClasses,
    "aria-label": iconDescription
  })), open && hasMountedTrigger && wrappedMenuBody));
});
OverflowMenu.propTypes = {
  /**
   * Specify how the trigger should align with the tooltip
   */
  align: deprecateValuesWithin.deprecateValuesWithin(PropTypes__default.default.oneOf(['top', 'top-left',
  // deprecated use top-start instead
  'top-right',
  // deprecated use top-end instead

  'bottom', 'bottom-left',
  // deprecated use bottom-start instead
  'bottom-right',
  // deprecated use bottom-end instead

  'left', 'left-bottom',
  // deprecated use left-end instead
  'left-top',
  // deprecated use left-start instead

  'right', 'right-bottom',
  // deprecated use right-end instead
  'right-top',
  // deprecated use right-start instead

  // new values to match floating-ui
  'top-start', 'top-end', 'bottom-start', 'bottom-end', 'left-end', 'left-start', 'right-end', 'right-start']), ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'], mapPopoverAlign.mapPopoverAlign),
  /**
   * Specify a label to be read by screen readers on the container node
   */
  ['aria-label']: PropTypes__default.default.string,
  /**
   * Deprecated, please use `aria-label` instead.
   * Specify a label to be read by screen readers on the container note.
   */
  ariaLabel: deprecate.deprecate(PropTypes__default.default.string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'),
  /**
   * The child nodes.
   */
  children: PropTypes__default.default.node,
  /**
   * The CSS class names.
   */
  className: PropTypes__default.default.string,
  /**
   * The menu direction.
   */
  direction: PropTypes__default.default.oneOf([FloatingMenu.DIRECTION_TOP, FloatingMenu.DIRECTION_BOTTOM]),
  /**
   * `true` if the menu alignment should be flipped.
   */
  flipped: PropTypes__default.default.bool,
  /**
   * Enable or disable focus trap behavior
   */
  focusTrap: PropTypes__default.default.bool,
  /**
   * The CSS class for the icon.
   */
  iconClass: PropTypes__default.default.string,
  /**
   * The icon description.
   */
  iconDescription: PropTypes__default.default.string,
  /**
   * The element ID.
   */
  id: PropTypes__default.default.string,
  /**
   * `true` to use the light version. For use on $ui-01 backgrounds only.
   * Don't use this to make OverflowMenu background color same as container background color.
   */
  light: deprecate.deprecate(PropTypes__default.default.bool, 'The `light` prop for `OverflowMenu` is no longer needed and has been deprecated. It will be removed in the next major release. Use the Layer component instead.'),
  /**
   * The adjustment in position applied to the floating menu.
   */
  menuOffset: PropTypes__default.default.oneOfType([PropTypes__default.default.shape({
    top: PropTypes__default.default.number.isRequired,
    left: PropTypes__default.default.number.isRequired
  }), PropTypes__default.default.func]),
  /**
   * The adjustment in position applied to the floating menu.
   */
  menuOffsetFlip: PropTypes__default.default.oneOfType([PropTypes__default.default.shape({
    top: PropTypes__default.default.number.isRequired,
    left: PropTypes__default.default.number.isRequired
  }), PropTypes__default.default.func]),
  /**
   * The class to apply to the menu options
   */
  menuOptionsClass: PropTypes__default.default.string,
  /**
   * The event handler for the `click` event.
   */
  onClick: PropTypes__default.default.func,
  /**
   * Function called when menu is closed
   */
  onClose: PropTypes__default.default.func,
  /**
   * The event handler for the `focus` event.
   */
  onFocus: PropTypes__default.default.func,
  /**
   * The event handler for the `keydown` event.
   */
  onKeyDown: PropTypes__default.default.func,
  /**
   * Function called when menu is opened
   */
  onOpen: PropTypes__default.default.func,
  /**
   * `true` if the menu should be open.
   */
  open: PropTypes__default.default.bool,
  /**
   * A component used to render an icon.
   */
  renderIcon: PropTypes__default.default.oneOfType([PropTypes__default.default.func, PropTypes__default.default.object]),
  /**
   * Specify a CSS selector that matches the DOM element that should
   * be focused when the OverflowMenu opens
   */
  selectorPrimaryFocus: PropTypes__default.default.string,
  /**
   * Specify the size of the OverflowMenu. Currently supports either `xs`, `sm`, `md` (default) or `lg` as an option.
   */
  size: PropTypes__default.default.oneOf(['xs', 'sm', 'md', 'lg'])
};

exports.OverflowMenu = OverflowMenu;
exports.default = OverflowMenu;
exports.getMenuOffset = getMenuOffset;
