/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

// A list of menu items.
//
// This component provides keyboard navigation amongst any focusable
// children.

const {
  Children,
  PureComponent,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const { div } = dom;

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  focusableSelector: "resource://devtools/client/shared/focus.mjs",
});

class MenuList extends PureComponent {
  static get propTypes() {
    return {
      // ID to assign to the list container.
      id: PropTypes.string,

      // Children of the list.
      children: PropTypes.any,

      // Called whenever there is a change to the hovered or selected child.
      // The callback is passed the ID of the highlighted child or null if no
      // child is highlighted.
      onHighlightedChildChange: PropTypes.func,
    };
  }

  constructor(props) {
    super(props);

    this.onKeyDown = this.onKeyDown.bind(this);
    this.onMouseOverOrFocus = this.onMouseOverOrFocus.bind(this);
    this.onMouseOutOrBlur = this.onMouseOutOrBlur.bind(this);
    this.notifyHighlightedChildChange =
      this.notifyHighlightedChildChange.bind(this);

    this.setWrapperRef = element => {
      this.wrapperRef = element;
    };
  }

  onMouseOverOrFocus(e) {
    this.notifyHighlightedChildChange(e.target.id);
  }

  onMouseOutOrBlur() {
    const hoveredElem = this.wrapperRef.querySelector(":hover");
    if (!hoveredElem) {
      this.notifyHighlightedChildChange(null);
    }
  }

  notifyHighlightedChildChange(id) {
    if (this.props.onHighlightedChildChange) {
      this.props.onHighlightedChildChange(id);
    }
  }

  onKeyDown(e) {
    // Check if the focus is in the list.
    if (
      !this.wrapperRef ||
      !this.wrapperRef.contains(e.target.ownerDocument.activeElement)
    ) {
      return;
    }

    const getTabList = () =>
      Array.from(this.wrapperRef.querySelectorAll(lazy.focusableSelector));

    switch (e.key) {
      case "Tab":
      case "ArrowUp":
      case "ArrowDown":
        {
          const tabList = getTabList();
          const currentElement = e.target.ownerDocument.activeElement;
          const currentIndex = tabList.indexOf(currentElement);
          if (currentIndex !== -1) {
            let nextIndex;
            if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
              nextIndex =
                currentIndex === tabList.length - 1 ? 0 : currentIndex + 1;
            } else {
              nextIndex =
                currentIndex === 0 ? tabList.length - 1 : currentIndex - 1;
            }
            tabList[nextIndex].focus();
            e.preventDefault();
          }
        }
        break;

      case "Home":
        {
          const firstItem = this.wrapperRef.querySelector(
            lazy.focusableSelector
          );
          if (firstItem) {
            firstItem.focus();
            e.preventDefault();
          }
        }
        break;

      case "End":
        {
          const tabList = getTabList();
          if (tabList.length) {
            tabList[tabList.length - 1].focus();
            e.preventDefault();
          }
        }
        break;
    }
  }

  render() {
    const attr = {
      role: "menu",
      ref: this.setWrapperRef,
      onKeyDown: this.onKeyDown,
      onMouseOver: this.onMouseOverOrFocus,
      onMouseOut: this.onMouseOutOrBlur,
      onFocus: this.onMouseOverOrFocus,
      onBlur: this.onMouseOutOrBlur,
      className: "menu-standard-padding",
    };

    if (this.props.id) {
      attr.id = this.props.id;
    }

    // Add padding for checkbox image if necessary.
    let hasCheckbox = false;
    Children.forEach(this.props.children, (child, i) => {
      if (child == null || typeof child == "undefined") {
        console.warn("MenuList children at index", i, "is", child);
        return;
      }

      if (typeof child?.props?.checked !== "undefined") {
        hasCheckbox = true;
      }
    });
    if (hasCheckbox) {
      attr.className = "checkbox-container menu-standard-padding";
    }

    return div(attr, this.props.children);
  }
}

module.exports = MenuList;
